@bivy/bivy 0.18.5 → 0.18.6-staging.2
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/dist/apps/annotate.js +13 -0
- package/dist/apps/gateway.js +3 -2
- package/dist/apps/inspector.js +6 -1
- package/dist/apps/preview-shell.js +2 -2
- package/dist/apps/screenshot.js +11 -1
- package/dist/apps/service.js +4 -3
- package/dist/protocol/client-command-schemas.js +1 -0
- package/package.json +1 -1
package/dist/apps/annotate.js
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { decodePng } from "./review.js";
|
|
5
5
|
import { encodePng } from "./rfb.js";
|
|
6
|
+
/** Scroll containers are page data, never executable capture instructions. */
|
|
7
|
+
export function readElementScrolls(input) {
|
|
8
|
+
if (input === undefined)
|
|
9
|
+
return [];
|
|
10
|
+
if (!Array.isArray(input) || input.length > 50)
|
|
11
|
+
throw new Error("Invalid scroll containers.");
|
|
12
|
+
return input.map((raw) => {
|
|
13
|
+
if (!raw || typeof raw.selector !== "string" || !raw.selector.length || raw.selector.length > 2048
|
|
14
|
+
|| ![raw.x, raw.y].every(n => typeof n === "number" && Number.isFinite(n) && Math.abs(n) <= 100_000))
|
|
15
|
+
throw new Error("Invalid scroll container.");
|
|
16
|
+
return { selector: raw.selector, x: raw.x, y: raw.y };
|
|
17
|
+
});
|
|
18
|
+
}
|
|
6
19
|
const MAX_STROKES = 50;
|
|
7
20
|
const MAX_POINTS = 10_000;
|
|
8
21
|
/** Validates strokes from the client (untrusted shape): bounded, finite. */
|
package/dist/apps/gateway.js
CHANGED
|
@@ -12,7 +12,7 @@ import { previewShell } from "./preview-shell.js";
|
|
|
12
12
|
import { displayViewer, DISPLAY_MENU_PATH, DISPLAY_SOCKET_PATH, DISPLAY_STATS_PATH, NOVNC_PATH } from "./display-viewer.js";
|
|
13
13
|
import { pressMenu, readMenuPath, readMenus } from "./menu.js";
|
|
14
14
|
import { inspectorScript } from "./inspector.js";
|
|
15
|
-
import { readStrokes } from "./annotate.js";
|
|
15
|
+
import { readStrokes, readElementScrolls } from "./annotate.js";
|
|
16
16
|
const MIME = {
|
|
17
17
|
".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".mjs": "text/javascript; charset=utf-8",
|
|
18
18
|
".css": "text/css; charset=utf-8", ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png",
|
|
@@ -783,6 +783,7 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
|
|
|
783
783
|
try {
|
|
784
784
|
const raw = input.mark;
|
|
785
785
|
const strokes = readStrokes(raw?.strokes);
|
|
786
|
+
const elementScrolls = readElementScrolls(raw?.elementScrolls);
|
|
786
787
|
const v = raw.viewport;
|
|
787
788
|
if (!Number.isFinite(v?.width) || !Number.isFinite(v?.height) || v.width < 40 || v.width > 4000 || v.height < 40 || v.height > 4000 || v.width * v.height > 4_000_000)
|
|
788
789
|
throw Error();
|
|
@@ -796,7 +797,7 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
|
|
|
796
797
|
const distance = strokes.reduce((total, s) => total + s.points.reduce((length, p, i) => i ? length + Math.hypot(p[0] - s.points[i - 1][0], p[1] - s.points[i - 1][1]) : length, 0), 0);
|
|
797
798
|
if (distance > 50_000)
|
|
798
799
|
throw Error();
|
|
799
|
-
mark = { appId: entry.app.id, viewId: entry.view.id, strokes, viewport: v, path: text(raw.path, 2048), scroll, dpr: 1, theme: raw.theme === "dark" ? "dark" : "light", signals: { unknown: true } };
|
|
800
|
+
mark = { appId: entry.app.id, viewId: entry.view.id, strokes, viewport: v, path: text(raw.path, 2048), scroll, elementScrolls, dpr: 1, theme: raw.theme === "dark" ? "dark" : "light", signals: { unknown: true } };
|
|
800
801
|
}
|
|
801
802
|
catch {
|
|
802
803
|
res.writeHead(400);
|
package/dist/apps/inspector.js
CHANGED
|
@@ -92,8 +92,13 @@ const signals=()=>{
|
|
|
92
92
|
const open=Boolean(document.querySelector('dialog[open],details[open],[aria-expanded="true"],[aria-modal="true"]'));
|
|
93
93
|
return {storage,cookies:Boolean(document.cookie),interacted,open,edited};
|
|
94
94
|
};
|
|
95
|
+
// Use a unique DOM path: repeated class names must not restore another panel.
|
|
96
|
+
const elementScrolls=()=>[...document.querySelectorAll('*')].filter(el=>el!==document.scrollingElement&&(el.scrollLeft||el.scrollTop)).slice(0,50).map(el=>{
|
|
97
|
+
const parts=[];for(let n=el;n&&n.nodeType===1;n=n.parentElement){if(n.id){parts.unshift('#'+CSS.escape(n.id));break;}parts.unshift(n.localName+':nth-child('+([...n.parentNode.children].indexOf(n)+1)+')');}
|
|
98
|
+
return {selector:parts.join(' > '),x:el.scrollLeft,y:el.scrollTop};
|
|
99
|
+
});
|
|
95
100
|
const drawState=()=>({type:'draw-state',scroll:{x:Math.round(scrollX),y:Math.round(scrollY)},viewport:{width:innerWidth,height:innerHeight},dpr:devicePixelRatio,
|
|
96
|
-
theme:matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light',path:location.pathname+location.search+location.hash,signals:signals()});
|
|
101
|
+
theme:matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light',path:location.pathname+location.search+location.hash,elementScrolls:elementScrolls(),signals:signals()});
|
|
97
102
|
/** Elements inside a mark (viewport rect): a grid sample, then the largest
|
|
98
103
|
* element under each hit that sits mostly inside the mark — circling a
|
|
99
104
|
* button names the button, not the bar it's in. A line through something
|
|
@@ -338,7 +338,7 @@ function startDraw(target){
|
|
|
338
338
|
if(target==='frame'&&metadata.inspect!==false){
|
|
339
339
|
const current=draw;
|
|
340
340
|
current.state.signals={unknown:true};
|
|
341
|
-
current.waiting=d=>{current.waiting=null;current.scroll=pos(d.scroll);current.state={viewport:{width:Math.round(Number(d.viewport?.width))||current.state.viewport.width,height:Math.round(Number(d.viewport?.height))||current.state.viewport.height},dpr:Number(d.dpr)||devicePixelRatio,theme:d.theme==='dark'?'dark':'light',path:safePath(d.path),signals:Object.fromEntries(Object.entries(d.signals||{}).map(([k,v])=>[k,v===true]))};renderInk();};
|
|
341
|
+
current.waiting=d=>{current.waiting=null;current.scroll=pos(d.scroll);current.state={viewport:{width:Math.round(Number(d.viewport?.width))||current.state.viewport.width,height:Math.round(Number(d.viewport?.height))||current.state.viewport.height},dpr:Number(d.dpr)||devicePixelRatio,theme:d.theme==='dark'?'dark':'light',path:safePath(d.path),elementScrolls:d.elementScrolls,signals:Object.fromEntries(Object.entries(d.signals||{}).map(([k,v])=>[k,v===true]))};renderInk();};
|
|
342
342
|
frame.contentWindow?.postMessage({type:'bivy:draw'},metadata.origin);
|
|
343
343
|
}
|
|
344
344
|
for(const b of $('draw-bar').querySelectorAll('[data-tool]'))b.setAttribute('aria-checked',String(b.dataset.tool===draw.tool));
|
|
@@ -428,7 +428,7 @@ $('draw-done').onclick=()=>{
|
|
|
428
428
|
+(els.length?els.map(el=>'- '+String(el.selector).slice(0,300)+(el.text?' ("'+String(el.text).slice(0,120)+'")':'')).join('\\n')+'\\n':'')
|
|
429
429
|
+'Marks ('+(d.target==='compare'?'screenshot':'page')+' px): '+d.strokes.map(bounds).join('; ');
|
|
430
430
|
const mark=Object.assign({path:st.path,viewport:st.viewport,dpr:st.dpr,strokes:d.strokes.map(s=>({tool:s.tool,points:s.points})),signals:st.signals},
|
|
431
|
-
d.target==='frame'?{scroll:d.scroll,theme:st.theme}:{compare:compareAfter});
|
|
431
|
+
d.target==='frame'?{scroll:d.scroll,elementScrolls:st.elementScrolls,theme:st.theme}:{compare:compareAfter});
|
|
432
432
|
// The marks stay on screen, frozen, while you add your words.
|
|
433
433
|
d.done=true;draw=null;
|
|
434
434
|
draft(context,false);
|
package/dist/apps/screenshot.js
CHANGED
|
@@ -166,6 +166,16 @@ export async function takeShots(views, request, outDir, chrome = findChrome()) {
|
|
|
166
166
|
await cdp.send("Page.navigate", { url: origin + request.path }, sessionId);
|
|
167
167
|
await loaded;
|
|
168
168
|
await new Promise((r) => setTimeout(r, 400)); // let fonts and first effects settle
|
|
169
|
+
// Match scrolling inside app panels as well as the document. Marks are
|
|
170
|
+
// viewport positions within these panels; subtract only document scroll.
|
|
171
|
+
let elementScrollsRestored;
|
|
172
|
+
if (request.elementScrolls?.length) {
|
|
173
|
+
const positions = JSON.stringify(request.elementScrolls);
|
|
174
|
+
await cdp.send("Runtime.evaluate", { expression: `(${positions}).forEach(p => { try { document.querySelector(p.selector)?.scrollTo({left:p.x,top:p.y,behavior:'instant'}); } catch {} })` }, sessionId);
|
|
175
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
176
|
+
const { result } = await cdp.send("Runtime.evaluate", { expression: `(${positions}).every(p => { try { const el=document.querySelector(p.selector); return el && Math.abs(el.scrollLeft-p.x)<=2 && Math.abs(el.scrollTop-p.y)<=2; } catch { return false; } })`, returnByValue: true }, sessionId);
|
|
177
|
+
elementScrollsRestored = result?.value === true;
|
|
178
|
+
}
|
|
169
179
|
let scroll;
|
|
170
180
|
if (request.scroll) {
|
|
171
181
|
const { result } = await cdp.send("Runtime.evaluate", { expression: `scrollTo(${Number(request.scroll.x) || 0}, ${Number(request.scroll.y) || 0}); [scrollX, scrollY]`, returnByValue: true }, sessionId);
|
|
@@ -175,7 +185,7 @@ export async function takeShots(views, request, outDir, chrome = findChrome()) {
|
|
|
175
185
|
const { data } = await cdp.send("Page.captureScreenshot", { format: "png" }, sessionId);
|
|
176
186
|
const file = path.join(outDir, `${entry.view.id.slice(0, 8)}-${width}-${theme}.png`);
|
|
177
187
|
fs.writeFileSync(file, Buffer.from(data, "base64"));
|
|
178
|
-
shots.push({ viewId: entry.view.id, view: entry.view.name, width, theme, file, ...(scroll ? { scroll } : {}) });
|
|
188
|
+
shots.push({ viewId: entry.view.id, view: entry.view.name, width, theme, file, ...(scroll ? { scroll } : {}), ...(elementScrollsRestored !== undefined ? { elementScrollsRestored } : {}) });
|
|
179
189
|
}
|
|
180
190
|
}
|
|
181
191
|
return shots;
|
package/dist/apps/service.js
CHANGED
|
@@ -5,7 +5,7 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { scanListeners } from "./listeners.js";
|
|
7
7
|
import { takeShots } from "./screenshot.js";
|
|
8
|
-
import { approximate, composite, readStrokes } from "./annotate.js";
|
|
8
|
+
import { approximate, composite, readStrokes, readElementScrolls } from "./annotate.js";
|
|
9
9
|
import { captureFrame, encodePng, sendInput } from "./rfb.js";
|
|
10
10
|
import { inputEvents, readAction } from "./input.js";
|
|
11
11
|
import { pressMenu, readMenuPath, readMenus } from "./menu.js";
|
|
@@ -435,6 +435,7 @@ export class AppService {
|
|
|
435
435
|
if (entry.view.kind !== "web")
|
|
436
436
|
throw new Error("Only previews can be drawn on.");
|
|
437
437
|
const strokes = readStrokes(input.strokes);
|
|
438
|
+
const elementScrolls = readElementScrolls(input.elementScrolls);
|
|
438
439
|
const whole = (n, min, max) => typeof n === "number" && Number.isFinite(n) && n >= min && n <= max;
|
|
439
440
|
// Compare's shot is drawn on at its on-screen size, which can be small.
|
|
440
441
|
if (!whole(input.viewport?.width, 40, 4000) || !whole(input.viewport?.height, 40, 8000))
|
|
@@ -466,7 +467,7 @@ export class AppService {
|
|
|
466
467
|
}
|
|
467
468
|
const outDir = path.join(os.tmpdir(), "bivy-shots", "annotate", entry.view.id);
|
|
468
469
|
const dpr = whole(input.dpr, 1, 3) ? Math.round(input.dpr * 4) / 4 : 2;
|
|
469
|
-
const run = this.shooting.catch(() => { }).then(() => this.screenshots.take([entry], { widths: [viewport.width], themes: [input.theme === "dark" ? "dark" : "light"], path: page, height: viewport.height, scale: dpr, scroll }, outDir));
|
|
470
|
+
const run = this.shooting.catch(() => { }).then(() => this.screenshots.take([entry], { widths: [viewport.width], themes: [input.theme === "dark" ? "dark" : "light"], path: page, height: viewport.height, scale: dpr, scroll, ...(elementScrolls.length ? { elementScrolls } : {}) }, outDir));
|
|
470
471
|
this.shooting = run;
|
|
471
472
|
const [shot] = await run;
|
|
472
473
|
if (!shot)
|
|
@@ -476,7 +477,7 @@ export class AppService {
|
|
|
476
477
|
const landed = shot.scroll ?? scroll;
|
|
477
478
|
const scrolledTo = Math.abs(landed.x - scroll.x) <= 2 && Math.abs(landed.y - scroll.y) <= 2;
|
|
478
479
|
// Marks stay on the content they were drawn on, wherever the retake scrolled.
|
|
479
|
-
return finish(base, landed, approximate("retake", input.signals, scrolledTo));
|
|
480
|
+
return finish(base, landed, approximate("retake", input.signals, scrolledTo && shot.elementScrollsRestored !== false));
|
|
480
481
|
}
|
|
481
482
|
/** The managed server's output, as an attachable terminal (starts it if needed). */
|
|
482
483
|
async logs(sessionId, appId, viewId) {
|
|
@@ -23,6 +23,7 @@ export const CLIENT_COMMAND_SCHEMAS = {
|
|
|
23
23
|
"apps.reviewMode": Type.Object({ ...request, ...session, appId: Type.String(), mode: Type.Union([Type.Literal("ready"), Type.Literal("every"), Type.Literal("off")]) }),
|
|
24
24
|
"apps.annotate": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String(), path: Type.Optional(Type.String({ maxLength: 2048 })),
|
|
25
25
|
viewport: Type.Object({ width: Type.Number(), height: Type.Number() }), scroll: Type.Optional(Type.Object({ x: Type.Number(), y: Type.Number() })),
|
|
26
|
+
elementScrolls: Type.Optional(Type.Array(Type.Object({ selector: Type.String({ minLength: 1, maxLength: 2048 }), x: Type.Number({ minimum: -100000, maximum: 100000 }), y: Type.Number({ minimum: -100000, maximum: 100000 }) }), { maxItems: 50 })),
|
|
26
27
|
dpr: Type.Optional(Type.Number()), theme: Type.Optional(Type.Union([Type.Literal("light"), Type.Literal("dark")])), compare: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
27
28
|
strokes: Type.Array(Type.Object({ tool: Type.Union([Type.Literal("pen"), Type.Literal("box")]), points: Type.Array(Type.Array(Type.Number(), { minItems: 2, maxItems: 2 }), { maxItems: 10000 }) }), { minItems: 1, maxItems: 50 }),
|
|
28
29
|
signals: Type.Optional(Type.Record(Type.String(), Type.Boolean())) }),
|