@chalky/chalky 0.1.6 → 0.1.7
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/agent/extensions/board/browser.js +142 -1
- package/agent/extensions/card/browser.js +49 -6
- package/agent/extensions/card/index.ts +4 -1
- package/agent/extensions/doodle/README.md +1 -1
- package/agent/extensions/doodle/browser.js +20 -18
- package/agent/extensions/frame/browser.js +5 -3
- package/agent/extensions/note/browser.js +19 -16
- package/agent/extensions/page/page.html +1 -1
- package/agent/extensions/screen/browser.js +2 -2
- package/agent/extensions/screen/index.ts +7 -5
- package/agent/extensions/version/browser.js +3 -3
- package/node_modules/xlsx/CHANGELOG.md +361 -0
- package/node_modules/xlsx/LICENSE +201 -0
- package/node_modules/xlsx/README.md +41 -0
- package/node_modules/xlsx/bin/xlsx.njs +310 -0
- package/node_modules/xlsx/bower.json +22 -0
- package/node_modules/xlsx/dist/LICENSE +201 -0
- package/node_modules/xlsx/dist/cpexcel.d.ts +39 -0
- package/node_modules/xlsx/dist/cpexcel.full.mjs +1502 -0
- package/node_modules/xlsx/dist/cpexcel.js +1506 -0
- package/node_modules/xlsx/dist/shim.min.js +2 -0
- package/node_modules/xlsx/dist/xlsx.core.min.js +18 -0
- package/node_modules/xlsx/dist/xlsx.core.min.map +1 -0
- package/node_modules/xlsx/dist/xlsx.extendscript.js +28259 -0
- package/node_modules/xlsx/dist/xlsx.full.min.js +24 -0
- package/node_modules/xlsx/dist/xlsx.full.min.map +1 -0
- package/node_modules/xlsx/dist/xlsx.mini.min.js +10 -0
- package/node_modules/xlsx/dist/xlsx.mini.min.map +1 -0
- package/node_modules/xlsx/dist/xlsx.zahl.js +4 -0
- package/node_modules/xlsx/dist/xlsx.zahl.mjs +2 -0
- package/node_modules/xlsx/dist/zahl.d.ts +4 -0
- package/node_modules/xlsx/package.json +183 -0
- package/node_modules/xlsx/types/index.d.ts +1065 -0
- package/node_modules/xlsx/types/tsconfig.json +15 -0
- package/node_modules/xlsx/xlsx.js +28103 -0
- package/node_modules/xlsx/xlsx.mjs +28228 -0
- package/node_modules/xlsx/xlsxworker.js +14 -0
- package/package.json +1 -1
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// The shelf: a dot per board (yours is bigger). Hover it and the dots open into thumbnails, with + for a new board;
|
|
10
10
|
// click one to go there. It stays clear of the chat panel: more thumbnails than fit scroll between ‹ and › (grey at
|
|
11
11
|
// the ends; a new board scrolls to the end). Drag a card by its label onto a thumbnail to move it to that board, where it was ({type: "card_board"}).
|
|
12
|
+
// Zoom: each board keeps its own (in this browser), as CSS zoom on #top, so places stay in main-space px; the control
|
|
13
|
+
// at the bottom right (−, %, +, fit), or Ctrl/⌘ + wheel (a trackpad pinch). See setZoom below.
|
|
12
14
|
// page messages board_view {board}: the board this page shows; board_add
|
|
13
15
|
// GET /board {boards, current} event "board" {boards, added?, by?}
|
|
14
16
|
const CSS = `
|
|
@@ -61,7 +63,22 @@ const CSS = `
|
|
|
61
63
|
.boards-shelf .slot.drop .mini { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
62
64
|
.boards-shelf .add { display: none; width: 122px; height: 70px; border-radius: 6px; /* a thumbnail's size, border included */ border: 1px dashed rgba(0, 0, 0, 0.25);
|
|
63
65
|
color: var(--muted); font-size: 13px; align-self: flex-start; margin-top: 3px; }
|
|
64
|
-
.boards-shelf.open .add { display: block; } /* only in the open shelf: at rest it's just the dots
|
|
66
|
+
.boards-shelf.open .add { display: block; } /* only in the open shelf: at rest it's just the dots */
|
|
67
|
+
/* zoom: CSS zoom on #top scales everything in the main space; its padding (room for the palette, rail, input) stays
|
|
68
|
+
the same on screen */
|
|
69
|
+
#top { padding: calc(var(--palette) / var(--zoom, 1)) calc((var(--rail) + 28px) / var(--zoom, 1))
|
|
70
|
+
calc(var(--bar) / var(--zoom, 1)) calc((var(--rail) + 28px) / var(--zoom, 1)); }
|
|
71
|
+
.boards-zoom { position: fixed; right: 16px; bottom: 24px; z-index: 25; display: flex; align-items: center; gap: 2px; padding: 3px;
|
|
72
|
+
border-radius: 12px; background: var(--glass); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
|
|
73
|
+
border: 1px solid var(--edge); box-shadow: var(--shadow); font-size: 12px; color: var(--soft); }
|
|
74
|
+
.boards-zoom button { height: 26px; min-width: 26px; padding: 0 6px; border-radius: 8px; font-variant-numeric: tabular-nums; }
|
|
75
|
+
.boards-zoom button:hover { background: var(--hover); color: var(--text); }
|
|
76
|
+
.boards-zoom .pct { min-width: 46px; }
|
|
77
|
+
.boards-zoom .fit { font-size: 15px; }
|
|
78
|
+
/* the hand: drag the board to move around it; what is on it doesn't react meanwhile (apps' frames included) */
|
|
79
|
+
#top.panning { cursor: grab; }
|
|
80
|
+
#top.panning.grabbing { cursor: grabbing; }
|
|
81
|
+
#top.panning * { pointer-events: none !important; }`;
|
|
65
82
|
|
|
66
83
|
export default async function (page) {
|
|
67
84
|
document.head.insertAdjacentHTML("beforeend", `<style>${CSS}</style>`);
|
|
@@ -136,6 +153,129 @@ export default async function (page) {
|
|
|
136
153
|
view.scrollLeft = s.offsetLeft - (view.clientWidth - s.offsetWidth) / 2;
|
|
137
154
|
}
|
|
138
155
|
|
|
156
|
+
// ---- zoom: each board's own, kept in this browser (localStorage) ----
|
|
157
|
+
// CSS zoom on #top: places stay in main-space px (what cards, frames and marks store), and #top's scrollLeft/Top and
|
|
158
|
+
// clientWidth are in them too. Window px -> main px is (x - left) / zoom + scroll: the other extensions divide by
|
|
159
|
+
// #top's style.zoom where they turn the pointer into main px. Ctrl/⌘ + wheel (a trackpad pinch) zooms at the
|
|
160
|
+
// pointer; the control at the bottom right: −, the percentage (click: back to 100%), +, and fit (the whole board).
|
|
161
|
+
const MIN = 0.25, MAX = 2, KEY = "chalky.board-zoom";
|
|
162
|
+
const shot = new URLSearchParams(location.search).has("shot"); // the versions' headless view: always 100%
|
|
163
|
+
let zooms = {};
|
|
164
|
+
try { zooms = JSON.parse(localStorage.getItem(KEY)) || {}; } catch {}
|
|
165
|
+
const zoomOf = () => parseFloat(top.style.zoom) || 1;
|
|
166
|
+
function setZoom(z, at, save = true) { // at: the window point that stays put (else the middle of the view)
|
|
167
|
+
z = shot ? 1 : Math.min(MAX, Math.max(MIN, Math.round(z * 100) / 100));
|
|
168
|
+
const m = top.getBoundingClientRect(), z0 = zoomOf();
|
|
169
|
+
at ??= { x: m.left + m.width / 2, y: m.top + m.height / 2 };
|
|
170
|
+
const p = { x: (at.x - m.left) / z0 + top.scrollLeft, y: (at.y - m.top) / z0 + top.scrollTop }; // in main px
|
|
171
|
+
top.style.zoom = z === 1 ? "" : z;
|
|
172
|
+
top.style.setProperty("--zoom", z);
|
|
173
|
+
if (z !== z0) { top.scrollLeft = p.x - (at.x - m.left) / z; top.scrollTop = p.y - (at.y - m.top) / z; }
|
|
174
|
+
pct.textContent = `${Math.round(z * 100)}%`;
|
|
175
|
+
if (save) {
|
|
176
|
+
if (z === 1) delete zooms[current]; else zooms[current] = z;
|
|
177
|
+
try { localStorage.setItem(KEY, JSON.stringify(zooms)); } catch {}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function fitZoom() { // everything on this board in view
|
|
181
|
+
const els = [...container(current).children], z0 = zoomOf(), m = top.getBoundingClientRect();
|
|
182
|
+
if (!els.length) return setZoom(1);
|
|
183
|
+
const rs = els.map((el) => el.getBoundingClientRect()).filter((r) => r.width || r.height);
|
|
184
|
+
const main = (x, y) => ({ x: (x - m.left) / z0 + top.scrollLeft, y: (y - m.top) / z0 + top.scrollTop });
|
|
185
|
+
const a = main(Math.min(...rs.map((r) => r.left)), Math.min(...rs.map((r) => r.top)));
|
|
186
|
+
const b = main(Math.max(...rs.map((r) => r.right)), Math.max(...rs.map((r) => r.bottom)));
|
|
187
|
+
const s = getComputedStyle(document.documentElement), px = (v) => parseFloat(s.getPropertyValue(v)) || 0;
|
|
188
|
+
const side = px("--rail") + 28, w = innerWidth - 2 * side, h = innerHeight - px("--palette") - px("--bar"); // inside the padding
|
|
189
|
+
const z = Math.min(MAX, Math.max(MIN, Math.min(w / (b.x - a.x + 24), h / (b.y - a.y + 24), 1)));
|
|
190
|
+
setZoom(z);
|
|
191
|
+
top.scrollLeft = (a.x + b.x) / 2 - (m.width / 2) / z;
|
|
192
|
+
top.scrollTop = a.y - 12 - px("--palette") / z;
|
|
193
|
+
}
|
|
194
|
+
const zoomBar = Object.assign(document.createElement("div"), { className: "boards-zoom" });
|
|
195
|
+
zoomBar.dataset.name = "zoom";
|
|
196
|
+
const pct = button("pct", "100%", "Back to 100%", () => setZoom(1));
|
|
197
|
+
zoomBar.append(button("", "−", "Zoom out", () => setZoom(zoomOf() / 1.25)), pct,
|
|
198
|
+
button("", "+", "Zoom in", () => setZoom(zoomOf() * 1.25)), button("fit", "⤢", "Fit the whole board", fitZoom));
|
|
199
|
+
document.body.append(zoomBar);
|
|
200
|
+
const pinch = (e, at) => { // ctrl + wheel (a pinch on a trackpad sends that): zoom at the pointer, not the browser's
|
|
201
|
+
if (!e.ctrlKey && !e.metaKey) return;
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
setZoom(zoomOf() * Math.exp(-e.deltaY * (e.deltaMode ? 0.05 : 0.01)), at);
|
|
204
|
+
};
|
|
205
|
+
// anywhere on the page, not only over the main space: over the messages panel (which lies over the board) a pinch
|
|
206
|
+
// would otherwise scroll the messages; captured, so nothing under the pointer scrolls with it
|
|
207
|
+
addEventListener("wheel", (e) => pinch(e, { x: e.clientX, y: e.clientY }), { passive: false, capture: true });
|
|
208
|
+
top.addEventListener("load", (e) => { // an app's frame (same site): its pinches too, its px turned into window px
|
|
209
|
+
const fr = e.target;
|
|
210
|
+
if (fr.localName !== "iframe") return;
|
|
211
|
+
try {
|
|
212
|
+
fr.contentWindow.addEventListener("wheel", (ev) => {
|
|
213
|
+
const r = fr.getBoundingClientRect(), k = zoomOf();
|
|
214
|
+
pinch(ev, { x: r.left + fr.clientLeft * k + ev.clientX * k, y: r.top + fr.clientTop * k + ev.clientY * k });
|
|
215
|
+
}, { passive: false });
|
|
216
|
+
} catch {} // another site's frame
|
|
217
|
+
}, true);
|
|
218
|
+
|
|
219
|
+
// ---- the hand: drag to move around the board (scrolls #top). The palette's hand tool (one tool at a time, Esc
|
|
220
|
+
// stops it); or hold Space (when not typing and no other tool is on); or drag with the middle button, any time.
|
|
221
|
+
// Two-finger scrolling on a trackpad moves around too (the browser's own scrolling).
|
|
222
|
+
const HAND = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 13V5.5a1.5 1.5 0 0 1 3 0V12"/>` +
|
|
223
|
+
`<path d="M11 5.5v-2a1.5 1.5 0 1 1 3 0V12"/><path d="M14 5.5a1.5 1.5 0 0 1 3 0V12"/>` +
|
|
224
|
+
`<path d="M17 7.5a1.5 1.5 0 0 1 3 0V16a6 6 0 0 1-6 6h-2 .2a6 6 0 0 1-5-2.7l-.2-.3c-.3-.5-1.4-2.4-3.3-5.7a1.5 1.5 0 0 1 .5-2 1.9 1.9 0 0 1 2.3.3L8 13"/></svg>`;
|
|
225
|
+
const handButton = Object.assign(document.createElement("button"), { innerHTML: HAND,
|
|
226
|
+
title: "Hand: drag to move around the board (or hold Space, or drag with the middle button; Esc to stop)" });
|
|
227
|
+
handButton.dataset.name = "hand";
|
|
228
|
+
handButton.style.order = 99; // the palette's last tool, at the right end, whichever script adds its tools after this one
|
|
229
|
+
page.slot("status").append(handButton);
|
|
230
|
+
let hand = false, spaceHand = false, panning = null; // panning: {x, y, left, top} at the press
|
|
231
|
+
const panMode = () => top.classList.toggle("panning", hand || spaceHand || !!panning);
|
|
232
|
+
function setHand(v) {
|
|
233
|
+
hand = v;
|
|
234
|
+
handButton.classList.toggle("on", v);
|
|
235
|
+
panMode();
|
|
236
|
+
if (v) document.dispatchEvent(new CustomEvent("tool-on", { detail: "hand" })); // the other tools turn off
|
|
237
|
+
}
|
|
238
|
+
handButton.onclick = () => setHand(!hand);
|
|
239
|
+
document.addEventListener("tool-on", (e) => { if (hand && e.detail !== "hand") setHand(false); });
|
|
240
|
+
const typing = () => document.activeElement?.matches?.("input, textarea, select, [contenteditable=''], [contenteditable='true']") ||
|
|
241
|
+
document.activeElement?.localName === "iframe"; // an app has the keys
|
|
242
|
+
const otherTool = () => !!document.querySelector("#status button.on:not([data-name='show-marks']):not([data-name='hand'])");
|
|
243
|
+
addEventListener("keydown", (e) => {
|
|
244
|
+
if (e.key === "Escape") setHand(false);
|
|
245
|
+
if (e.code !== "Space") return;
|
|
246
|
+
if (spaceHand) return e.preventDefault(); // held: its repeats don't scroll the page either
|
|
247
|
+
if (typing() || otherTool() || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
248
|
+
e.preventDefault(); // not a page scroll, nor a click on the focused button
|
|
249
|
+
spaceHand = true;
|
|
250
|
+
panMode();
|
|
251
|
+
}, true);
|
|
252
|
+
addEventListener("keyup", (e) => { if (e.code === "Space" && spaceHand) { spaceHand = false; panMode(); } }, true);
|
|
253
|
+
addEventListener("blur", () => { spaceHand = false; panMode(); });
|
|
254
|
+
top.addEventListener("pointerdown", (e) => {
|
|
255
|
+
if (!(e.button === 1 || (e.button === 0 && (hand || spaceHand)))) return;
|
|
256
|
+
e.preventDefault();
|
|
257
|
+
e.stopImmediatePropagation(); // not a click, a line or a frame on what is under it
|
|
258
|
+
panning = { x: e.clientX, y: e.clientY, left: top.scrollLeft, top: top.scrollTop, id: e.pointerId };
|
|
259
|
+
top.setPointerCapture(e.pointerId);
|
|
260
|
+
top.classList.add("grabbing");
|
|
261
|
+
panMode();
|
|
262
|
+
}, true);
|
|
263
|
+
top.addEventListener("pointermove", (e) => {
|
|
264
|
+
if (!panning || e.pointerId !== panning.id) return;
|
|
265
|
+
const z = zoomOf(); // window px moved -> main px scrolled, so the board stays under the pointer
|
|
266
|
+
top.scrollLeft = panning.left - (e.clientX - panning.x) / z;
|
|
267
|
+
top.scrollTop = panning.top - (e.clientY - panning.y) / z;
|
|
268
|
+
});
|
|
269
|
+
const stopPan = (e) => {
|
|
270
|
+
if (!panning || e.pointerId !== panning.id) return;
|
|
271
|
+
panning = null;
|
|
272
|
+
top.classList.remove("grabbing");
|
|
273
|
+
panMode();
|
|
274
|
+
};
|
|
275
|
+
top.addEventListener("pointerup", stopPan);
|
|
276
|
+
top.addEventListener("pointercancel", stopPan);
|
|
277
|
+
top.addEventListener("auxclick", (e) => { if (e.button === 1) e.preventDefault(); }); // the middle button doesn't open links
|
|
278
|
+
|
|
139
279
|
const others = document.createElement("style"); // hides what is on the body for other boards (a note's pin)
|
|
140
280
|
document.head.append(others);
|
|
141
281
|
function show(n, tell = true) { // go to board n (tell: the server, which the first show got it from)
|
|
@@ -145,6 +285,7 @@ export default async function (page) {
|
|
|
145
285
|
document.body.dataset.board = n;
|
|
146
286
|
others.textContent = `body > [data-board]:not([data-board="${n}"]) { display: none !important; }`;
|
|
147
287
|
for (const b of boards) container(b).classList.toggle("on", b === n);
|
|
288
|
+
setZoom(zooms[n] ?? 1, null, false); // each board keeps its own zoom (in this browser)
|
|
148
289
|
if (tell) page.send({ type: "board_view", board: n });
|
|
149
290
|
render();
|
|
150
291
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// A card is bare: only what the agent built, no frame (its label shows on hover: name, owner, ✕). It is
|
|
4
4
|
// stacked in the main space, its content keeping its own size; or pinned at place {x, y, width?, height?}
|
|
5
5
|
// (px from the main space's top-left, so it scrolls with the page), its root block then filling it.
|
|
6
|
+
// (a new card with no place gets one near the middle of the view: autoPlace)
|
|
6
7
|
// What the user can do with the label (the handle, so clicks inside still work):
|
|
7
8
|
// drag move it; a drop sends {type: "card_move", card, place} (unless draggable: false)
|
|
8
9
|
// drag and hold still over another card (it lights up), release combine: it slides back and the page
|
|
@@ -287,11 +288,11 @@ export default async function (page) {
|
|
|
287
288
|
|
|
288
289
|
page.on("card", (ev) => {
|
|
289
290
|
if (ev.op === "render") {
|
|
290
|
-
const old = cards[ev.card] || {};
|
|
291
|
+
const isNew = !cards[ev.card], old = cards[ev.card] || {};
|
|
291
292
|
const { op, type, card, ...record } = ev;
|
|
292
293
|
cards[ev.card] = { ...record, box: old.box, label: old.label, body: old.body, nodes: old.nodes, frame: old.frame,
|
|
293
294
|
flipped: old.flipped, option: record.options ? old.option : 0 };
|
|
294
|
-
draw(ev.card);
|
|
295
|
+
draw(ev.card).then(() => { if (isNew && !record.place) autoPlace(ev.card); });
|
|
295
296
|
} else if (ev.op === "delete" && cards[ev.card]) {
|
|
296
297
|
cards[ev.card].box?.remove();
|
|
297
298
|
delete cards[ev.card];
|
|
@@ -306,6 +307,47 @@ export default async function (page) {
|
|
|
306
307
|
}
|
|
307
308
|
});
|
|
308
309
|
|
|
310
|
+
// A new card the agent gave no place: near the middle of what the user sees (between the panel, if open, and the
|
|
311
|
+
// right edge; below the palette, above the input bar), clear of everything else on the board. The nearest free
|
|
312
|
+
// spot to the middle, on a 40 px grid, keeping a 24 px gap. Only the tab in view, and only on the board shown
|
|
313
|
+
// (a card for another board stays stacked). Sent as card_place: saved, with no step of its own in the history.
|
|
314
|
+
const GAP = 24, STEP = 40, RINGS = 60;
|
|
315
|
+
async function autoPlace(id) {
|
|
316
|
+
const s = cards[id];
|
|
317
|
+
if (!s?.box || s.place || document.visibilityState !== "visible") return;
|
|
318
|
+
if ((Number(s.board) || 1) !== (Number(document.body.dataset.board) || 1)) return;
|
|
319
|
+
await new Promise(requestAnimationFrame); // its blocks drawn
|
|
320
|
+
if (s.place) return;
|
|
321
|
+
const box = s.box;
|
|
322
|
+
box.toggleAttribute("data-placed", true); // its size as a placed card, off the stack
|
|
323
|
+
Object.assign(box.style, { position: "absolute", margin: "0", left: "0px", top: "0px", visibility: "hidden" });
|
|
324
|
+
const z = parseFloat(area.style.zoom) || 1, w = box.offsetWidth, h = box.offsetHeight;
|
|
325
|
+
const m = area.getBoundingClientRect();
|
|
326
|
+
const main = (x, y) => ({ x: (x - m.left) / z + area.scrollLeft, y: (y - m.top) / z + area.scrollTop });
|
|
327
|
+
const css = getComputedStyle(document.documentElement), px = (v) => parseFloat(css.getPropertyValue(v)) || 0;
|
|
328
|
+
const panel = document.getElementById("panel"), open = ["messages", "versions"].includes(document.body.dataset.panel);
|
|
329
|
+
const side = px("--rail") + 28;
|
|
330
|
+
const a = main(Math.max(side, open && panel ? panel.getBoundingClientRect().right : 0), px("--palette"));
|
|
331
|
+
const b = main(innerWidth - side, innerHeight - px("--bar"));
|
|
332
|
+
const cx = (a.x + b.x) / 2 - w / 2, cy = (a.y + b.y) / 2 - h / 2;
|
|
333
|
+
const others = [...(box.parentElement?.children ?? [])].filter((el) => el !== box).map((el) => el.getBoundingClientRect())
|
|
334
|
+
.filter((r) => r.width && r.height).map((r) => ({ ...main(r.left, r.top), w: r.width / z, h: r.height / z }));
|
|
335
|
+
const free = (x, y) => others.every((o) => x + w + GAP <= o.x || o.x + o.w + GAP <= x || y + h + GAP <= o.y || o.y + o.h + GAP <= y);
|
|
336
|
+
const spots = [];
|
|
337
|
+
for (let i = -RINGS; i <= RINGS; i++) for (let j = -RINGS; j <= RINGS; j++) {
|
|
338
|
+
const x = Math.round(cx + i * STEP), y = Math.round(cy + j * STEP);
|
|
339
|
+
if (x >= 0 && y >= 0) spots.push({ x, y, d: (i * i + j * j) });
|
|
340
|
+
}
|
|
341
|
+
spots.sort((p, q) => p.d - q.d);
|
|
342
|
+
const spot = spots.find((p) => free(p.x, p.y)) ?? { x: Math.max(0, Math.round(cx)), y: Math.max(0, Math.round(cy)) };
|
|
343
|
+
// an app keeps its width: a placed box with none shrinks to the room left of the window's edge, and in a narrower
|
|
344
|
+
// window (the card_show check's browser) an app far right would be 0px wide
|
|
345
|
+
s.place = s.app ? { x: spot.x, y: spot.y, width: w } : { x: spot.x, y: spot.y };
|
|
346
|
+
Object.assign(box.style, { left: `${spot.x}px`, top: `${spot.y}px`, visibility: "" });
|
|
347
|
+
if (s.app) { box.style.width = `${w}px`; s.frame && (s.frame.style.width = "100%"); }
|
|
348
|
+
page.send({ type: "card_place", card: id, place: s.place });
|
|
349
|
+
}
|
|
350
|
+
|
|
309
351
|
function pinStacked() { // pin each stacked card where it is now, so taking one out of the stack moves none
|
|
310
352
|
const stacked = Object.entries(cards).filter(([, s]) => !s.place && s.box);
|
|
311
353
|
for (const [, s] of stacked) s.place = { x: s.box.offsetLeft, y: s.box.offsetTop }; // read all, then move
|
|
@@ -325,6 +367,7 @@ export default async function (page) {
|
|
|
325
367
|
const handle = e.currentTarget, box = s.box;
|
|
326
368
|
handle.setPointerCapture(e.pointerId); // keep getting moves over iframes and other cards
|
|
327
369
|
let place = null, held = false, x0 = 0, y0 = 0;
|
|
370
|
+
const z = parseFloat(area.style.zoom) || 1; // the board's zoom (board/browser.js): window px moved -> main px
|
|
328
371
|
let under = null, armed = null, wait = null, sx = 0, sy = 0; // the card under the pointer; armed: lit, ready to combine
|
|
329
372
|
const hold = setTimeout(() => { held = true; page.send({ type: "card_pin", card: id, pinned: !s.pinned }); }, HOLD_MS);
|
|
330
373
|
handle.onpointermove = (ev) => {
|
|
@@ -332,7 +375,7 @@ export default async function (page) {
|
|
|
332
375
|
clearTimeout(hold);
|
|
333
376
|
if (s.pinned || s.draggable === false) return;
|
|
334
377
|
if (!place) { pinStacked(); x0 = box.offsetLeft; y0 = box.offsetTop; box.style.zIndex = 4; } // offsets in the main space; above the rest while dragged
|
|
335
|
-
place = { ...s.place, x: Math.round(x0 + ev.clientX - e.clientX), y: Math.round(y0 + ev.clientY - e.clientY) };
|
|
378
|
+
place = { ...s.place, x: Math.round(x0 + (ev.clientX - e.clientX) / z), y: Math.round(y0 + (ev.clientY - e.clientY) / z) };
|
|
336
379
|
box.toggleAttribute("data-placed", true);
|
|
337
380
|
Object.assign(box.style, { position: "absolute", margin: "0", left: `${place.x}px`, top: `${place.y}px` });
|
|
338
381
|
const now = cardAt(ev.clientX, ev.clientY, box);
|
|
@@ -366,12 +409,12 @@ export default async function (page) {
|
|
|
366
409
|
if (s.pinned) return;
|
|
367
410
|
pinStacked();
|
|
368
411
|
const handle = e.currentTarget, box = s.box;
|
|
369
|
-
const x = box.offsetLeft, y = box.offsetTop, w0 = box.offsetWidth, h0 = box.offsetHeight;
|
|
412
|
+
const x = box.offsetLeft, y = box.offsetTop, w0 = box.offsetWidth, h0 = box.offsetHeight, z = parseFloat(area.style.zoom) || 1;
|
|
370
413
|
handle.setPointerCapture(e.pointerId);
|
|
371
414
|
let place = null;
|
|
372
415
|
handle.onpointermove = (ev) => {
|
|
373
|
-
place = { x, y, width: Math.min(area.clientWidth - x, Math.max(40, Math.round(w0 + ev.clientX - e.clientX))),
|
|
374
|
-
height: Math.max(30, Math.round(h0 + ev.clientY - e.clientY)) };
|
|
416
|
+
place = { x, y, width: Math.min(area.clientWidth - x, Math.max(40, Math.round(w0 + (ev.clientX - e.clientX) / z))),
|
|
417
|
+
height: Math.max(30, Math.round(h0 + (ev.clientY - e.clientY) / z)) };
|
|
375
418
|
box.toggleAttribute("data-placed", true);
|
|
376
419
|
Object.assign(box.style, { position: "absolute", margin: "0", left: `${x}px`, top: `${y}px`,
|
|
377
420
|
width: `${place.width}px`, height: `${place.height}px` });
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// GET /card/all every card GET /card/about block name -> its about block
|
|
17
17
|
// GET /card/blocks block name -> mtime GET /card/blocks/<Name>.js, /card/node_modules/{marked,dompurify}/...
|
|
18
18
|
// GET /card/lib/... libraries the agents added for blocks (lib/ here: ES-module builds)
|
|
19
|
-
// page messages card_set, card_prop, card_move, card_board, card_pin, card_keep, card_close, card_action
|
|
19
|
+
// page messages card_set, card_prop, card_move, card_place, card_board, card_pin, card_keep, card_close, card_action
|
|
20
20
|
// page event "card" {op: "render", card, ...record} | {op: "delete", card} | {op: "reset", cards}
|
|
21
21
|
import fs from "node:fs";
|
|
22
22
|
import path from "node:path";
|
|
@@ -344,6 +344,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
344
344
|
else if (type === "card_move") {
|
|
345
345
|
if (r.pinned) return host.broadcast("card", { op: "render", card: id, ...r });
|
|
346
346
|
r.place = msg.place; cards.change(id, r, "user", `moved ${id}`);
|
|
347
|
+
} else if (type === "card_place") { // the page put a new card with no place near the middle of the view: not a move
|
|
348
|
+
if (r.place || r.pinned) return; // the user saw it appear there, so no step of its own (it is part of the showing)
|
|
349
|
+
r.place = msg.place; cards.change(id, r);
|
|
347
350
|
} else if (type === "card_board" && host.api.board?.has(msg.board)) { // dropped on another board's thumbnail
|
|
348
351
|
if (r.pinned) return host.broadcast("card", { op: "render", card: id, ...r });
|
|
349
352
|
r.board = msg.board; r.place = msg.place ?? null; cards.change(id, r, "user", `moved ${id} to board ${msg.board}`);
|
|
@@ -29,7 +29,7 @@ You name the thing; the page draws it by hand in your colour, and keeps it on th
|
|
|
29
29
|
page (the mark goes on just those characters), element a CSS selector or an element's id or data-name (also inside
|
|
30
30
|
an app's frame), mark one of the marks (theirs or yours: m7). Example: {"card": "shop", "text": "12.50"}.
|
|
31
31
|
- The page's own controls can be marked too, by name: {element: "pen"} (the marker), "text", "eraser", "show-marks"
|
|
32
|
-
(the eye), "note" (the
|
|
32
|
+
(the eye), "note" (the speech bubble), "frames", "files", "dictate" (the mic), "tools" (the whole palette, top centre),
|
|
33
33
|
"rail" (left edge), "messages", "versions", "message-line" (bottom), "boards" (the shelf above it), "new-board". Such
|
|
34
34
|
marks stay fixed to the window. Good for a tour: ring "pen" with "draw here".
|
|
35
35
|
- at: {x, y, width, height} in window px, as screen_see's picture shows it: a last resort for something with no name
|
|
@@ -175,13 +175,14 @@ export default async function (page) {
|
|
|
175
175
|
const picture = (full, box) => screen.picture(full, box); // crop a frame to box (window px) and upload it: {path, url}
|
|
176
176
|
|
|
177
177
|
// ---- where things are ----
|
|
178
|
+
const zoom = () => parseFloat(area.style.zoom) || 1; // the board's zoom (board/browser.js): window px per main px (and per app-frame px)
|
|
178
179
|
const inMain = (x, y) => { // window px -> main-space px (as note/browser.js)
|
|
179
|
-
const m = area.getBoundingClientRect();
|
|
180
|
-
return { x: x - m.left + area.scrollLeft, y: y - m.top + area.scrollTop };
|
|
180
|
+
const m = area.getBoundingClientRect(), z = zoom();
|
|
181
|
+
return { x: (x - m.left) / z + area.scrollLeft, y: (y - m.top) / z + area.scrollTop };
|
|
181
182
|
};
|
|
182
183
|
const inWindow = (p) => {
|
|
183
|
-
const m = area.getBoundingClientRect();
|
|
184
|
-
return { x:
|
|
184
|
+
const m = area.getBoundingClientRect(), z = zoom();
|
|
185
|
+
return { x: m.left + (p.x - area.scrollLeft) * z, y: m.top + (p.y - area.scrollTop) * z };
|
|
185
186
|
};
|
|
186
187
|
const short = (s, n = 40) => { s = (s || "").replace(/\s+/g, " ").trim(); return s.length > n ? s.slice(0, n - 1) + "…" : s; };
|
|
187
188
|
|
|
@@ -266,7 +267,7 @@ export default async function (page) {
|
|
|
266
267
|
const at = inMain(box.x, box.y); // the part of the main space the picture shows, so the agent can place things
|
|
267
268
|
const step = { drawn: marks.length > 0, words: marks.filter((m) => m.text).map((m) => m.text), then: [], shot: null,
|
|
268
269
|
board: Number(document.body.dataset.board) || 1, // the board shown (board/)
|
|
269
|
-
area: { x: Math.round(at.x), y: Math.round(at.y), width: Math.round(box.w), height: Math.round(box.h) } };
|
|
270
|
+
area: { x: Math.round(at.x), y: Math.round(at.y), width: Math.round(box.w / zoom()), height: Math.round(box.h / zoom()) } };
|
|
270
271
|
step.ready = full ? picture(full, box).then((s) => { step.shot = s; }, () => {}) : Promise.resolve();
|
|
271
272
|
steps.push(step);
|
|
272
273
|
for (const m of marks) m.el.remove();
|
|
@@ -303,10 +304,10 @@ export default async function (page) {
|
|
|
303
304
|
doc.head.append(s);
|
|
304
305
|
classes(doc.documentElement);
|
|
305
306
|
}
|
|
306
|
-
const offset = () => {
|
|
307
|
-
if (!frameEl) return { x: 0, y: 0 };
|
|
308
|
-
const r = frameEl.getBoundingClientRect();
|
|
309
|
-
return { x: r.left + frameEl.clientLeft, y: r.top + frameEl.clientTop };
|
|
307
|
+
const offset = () => { // an app frame's (0, 0) in window px, and window px per frame px (k: zoomed with the main space)
|
|
308
|
+
if (!frameEl) return { x: 0, y: 0, k: 1 };
|
|
309
|
+
const r = frameEl.getBoundingClientRect(), k = zoom();
|
|
310
|
+
return { x: r.left + frameEl.clientLeft * k, y: r.top + frameEl.clientTop * k, k };
|
|
310
311
|
};
|
|
311
312
|
const root = frameEl ? doc : area;
|
|
312
313
|
root.addEventListener("pointerdown", (e) => {
|
|
@@ -316,16 +317,16 @@ export default async function (page) {
|
|
|
316
317
|
if (erasing) { // the press is for the eraser, not for what's under it
|
|
317
318
|
e.preventDefault();
|
|
318
319
|
press = { erase: true, win };
|
|
319
|
-
eraseAt(inMain(e.clientX
|
|
320
|
+
eraseAt(inMain(o.x + e.clientX * o.k, o.y + e.clientY * o.k));
|
|
320
321
|
return;
|
|
321
322
|
}
|
|
322
323
|
grabbed = marks.length ? frame() : null; // before the click acts
|
|
323
324
|
if (e.target.closest?.(texting && !drawing ? ".doodle-note" : OWN_DRAG)) return;
|
|
324
325
|
if (texting) e.preventDefault(); // the press is for the words (or a line), not for what's under it
|
|
325
|
-
press = { x: e.clientX
|
|
326
|
+
press = { x: o.x + e.clientX * o.k, y: o.y + e.clientY * o.k, id: e.pointerId, target: e.target, win, o, text: texting };
|
|
326
327
|
}, true);
|
|
327
328
|
win.addEventListener("pointermove", (e) => {
|
|
328
|
-
const o = offset(), x = e.clientX
|
|
329
|
+
const o = offset(), x = o.x + e.clientX * o.k, y = o.y + e.clientY * o.k;
|
|
329
330
|
over = frameEl || e.target.closest?.("#top") ? { x, y } : null;
|
|
330
331
|
if (press?.erase && press.win === win) { win.getSelection().removeAllRanges(); eraseAt(inMain(x, y)); return; }
|
|
331
332
|
if (!press || press.win !== win || !drawing) return;
|
|
@@ -495,7 +496,7 @@ export default async function (page) {
|
|
|
495
496
|
hideAsk();
|
|
496
497
|
const last = marks.at(-1).points.at(-1), to = document.getElementById("to")?.value || "agent1";
|
|
497
498
|
ask = Object.assign(document.createElement("form"), { className: "doodle-ask" });
|
|
498
|
-
const m = area.getBoundingClientRect(), room = inMain(m.left + area.clientWidth - 316, m.bottom - 140); // inside the view (not under the scrollbar)
|
|
499
|
+
const m = area.getBoundingClientRect(), z = zoom(), room = inMain(m.left + (area.clientWidth - 316) * z, m.bottom - 140 * z); // inside the view (not under the scrollbar)
|
|
499
500
|
Object.assign(ask.style, { left: `${Math.min(last.x + 16, room.x)}px`, top: `${Math.min(last.y + 12, room.y)}px` });
|
|
500
501
|
const field = Object.assign(document.createElement("input"), { placeholder: `Say something… (Enter sends to ${to})`, autocomplete: "off" });
|
|
501
502
|
field.setAttribute("aria-label", "Words to send with your marks");
|
|
@@ -527,10 +528,11 @@ export default async function (page) {
|
|
|
527
528
|
const distBox = (b, q) => Math.hypot(Math.max(b.x - q.x, 0, q.x - b.x - b.w), Math.max(b.y - q.y, 0, q.y - b.y - b.h));
|
|
528
529
|
const inPoly = (pts, q) => { let c = false; for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) { const a = pts[i], b = pts[j];
|
|
529
530
|
if ((a.y > q.y) !== (b.y > q.y) && q.x < (b.x - a.x) * (q.y - a.y) / (b.y - a.y) + a.x) c = !c; } return c; };
|
|
530
|
-
const frameOffset = (fr) => { const r = fr.getBoundingClientRect(); return { x: r.left + fr.clientLeft, y: r.top + fr.clientTop }; };
|
|
531
|
-
function rectOf(el, fr) { // an element (or a Range), in main px; fr: the app frame it is in
|
|
532
|
-
const r = el.getBoundingClientRect(), o = fr ? frameOffset(fr) : { x: 0, y: 0 }, p = inMain(r.left
|
|
533
|
-
|
|
531
|
+
const frameOffset = (fr) => { const r = fr.getBoundingClientRect(), k = zoom(); return { x: r.left + fr.clientLeft * k, y: r.top + fr.clientTop * k, k }; };
|
|
532
|
+
function rectOf(el, fr) { // an element (or a Range), in main px; fr: the app frame it is in (its px: main px)
|
|
533
|
+
const r = el.getBoundingClientRect(), o = fr ? frameOffset(fr) : { x: 0, y: 0, k: 1 }, p = inMain(o.x + r.left * o.k, o.y + r.top * o.k);
|
|
534
|
+
const s = fr ? 1 : 1 / zoom(); // window px -> main px
|
|
535
|
+
return { x: p.x, y: p.y, w: r.width * s, h: r.height * s };
|
|
534
536
|
}
|
|
535
537
|
const scopes = (el) => [{ root: el, fr: null }, ...[...(el.localName === "iframe" ? [el] : el.querySelectorAll("iframe"))].flatMap((fr) => {
|
|
536
538
|
try { return fr.contentDocument?.body ? [{ root: fr.contentDocument.body, fr }] : []; } catch { return []; } // other origins: not ours
|
|
@@ -671,7 +673,7 @@ export default async function (page) {
|
|
|
671
673
|
if (under && !area.contains(under) && under !== document.documentElement && under !== document.body)
|
|
672
674
|
return { box: { x, y, w, h }, board: Number(board()), fixed: true };
|
|
673
675
|
const p = inMain(x, y);
|
|
674
|
-
return { box: { x: p.x, y: p.y, w, h }, board: Number(board()) };
|
|
676
|
+
return { box: { x: p.x, y: p.y, w: w / zoom(), h: h / zoom() }, board: Number(board()) };
|
|
675
677
|
}
|
|
676
678
|
function findMain(t, every) {
|
|
677
679
|
if (t.mark) {
|
|
@@ -71,9 +71,10 @@ export default async function (page) {
|
|
|
71
71
|
const board = () => Number(document.body.dataset.board) || 1; // the board shown (board/): frames are drawn and sent there
|
|
72
72
|
document.addEventListener("board", () => setTimeout(() => render(shown))); // after the switch: count that board's
|
|
73
73
|
|
|
74
|
+
const zoom = () => parseFloat(area.style.zoom) || 1; // the board's zoom (board/browser.js)
|
|
74
75
|
const inMain = (x, y) => { // window px -> main-space px (as note/browser.js)
|
|
75
|
-
const m = area.getBoundingClientRect();
|
|
76
|
-
return { x: Math.round(x - m.left + area.scrollLeft), y: Math.round(y - m.top + area.scrollTop) };
|
|
76
|
+
const m = area.getBoundingClientRect(), z = zoom();
|
|
77
|
+
return { x: Math.round((x - m.left) / z + area.scrollLeft), y: Math.round((y - m.top) / z + area.scrollTop) };
|
|
77
78
|
};
|
|
78
79
|
const put = (box, f) => Object.assign(box.style, { left: `${f.x}px`, top: `${f.y}px`, width: `${f.width}px`, height: `${f.height}px` });
|
|
79
80
|
|
|
@@ -154,8 +155,9 @@ export default async function (page) {
|
|
|
154
155
|
const corner = e.currentTarget, x = box.offsetLeft, y = box.offsetTop, w0 = box.offsetWidth, h0 = box.offsetHeight;
|
|
155
156
|
corner.setPointerCapture(e.pointerId);
|
|
156
157
|
let place = null;
|
|
158
|
+
const z = zoom(); // window px moved -> main px
|
|
157
159
|
corner.onpointermove = (ev) => {
|
|
158
|
-
place = { x, y, width: Math.max(40, Math.round(w0 + ev.clientX - e.clientX)), height: Math.max(30, Math.round(h0 + ev.clientY - e.clientY)) };
|
|
160
|
+
place = { x, y, width: Math.max(40, Math.round(w0 + (ev.clientX - e.clientX) / z)), height: Math.max(30, Math.round(h0 + (ev.clientY - e.clientY) / z)) };
|
|
159
161
|
put(box, place);
|
|
160
162
|
};
|
|
161
163
|
corner.onpointerup = () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// The page side of the note extension. A
|
|
2
|
-
// layer over the page; the cursor is a
|
|
1
|
+
// The page side of the note extension. A speech-bubble button in the tool palette (top) turns on a clear
|
|
2
|
+
// layer over the page; the cursor is a speech bubble, blue over something a click would point at.
|
|
3
3
|
// Click: a note box opens there. Hold the button and draw: on release the note box opens, and the note
|
|
4
4
|
// covers the drawn area (its bounding box, in main-space px: where card_show's place puts a card).
|
|
5
5
|
// The user sees only the box; the target goes to the agent with the note: card and block (from
|
|
@@ -28,11 +28,11 @@ export default async function (page) {
|
|
|
28
28
|
|
|
29
29
|
const layer = document.createElement("div"); // catches clicks, so the page underneath doesn't react
|
|
30
30
|
layer.style.cssText = "position:fixed;inset:0;z-index:9998;display:none";
|
|
31
|
-
// The cursor: the button's
|
|
31
|
+
// The cursor: the button's speech bubble (white edge, so it shows on dark too); its tail's tip (2, 22) is the spot pointed at.
|
|
32
|
+
const BUBBLE = "M7.9 20A9 9 0 1 0 4 16.1L2 22z";
|
|
32
33
|
const pen = (color) => `url("data:image/svg+xml,${encodeURIComponent(
|
|
33
34
|
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round">` +
|
|
34
|
-
`<
|
|
35
|
-
`<g stroke="${color}" stroke-width="2"><path d="M4 20h4L19 9l-4-4L4 16z"/><path d="m13 7 4 4"/></g></svg>`)}") 4 20, crosshair`;
|
|
35
|
+
`<path d="${BUBBLE}" stroke="#fff" stroke-width="4"/><path d="${BUBBLE}" stroke="${color}" stroke-width="2"/></svg>`)}") 2 22, crosshair`;
|
|
36
36
|
const PEN = pen("#6e6e73"), PEN_ON = pen("#0071e3"); // grey: empty space; blue: over a thing
|
|
37
37
|
layer.style.cursor = PEN;
|
|
38
38
|
const input = document.createElement("input"); // the note box, opened where the user clicked
|
|
@@ -60,7 +60,7 @@ export default async function (page) {
|
|
|
60
60
|
document.body.append(layer, ink, input);
|
|
61
61
|
|
|
62
62
|
const button = document.createElement("button");
|
|
63
|
-
button.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
|
|
63
|
+
button.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="${BUBBLE}"/></svg>`; // a speech bubble
|
|
64
64
|
button.title = "Point: click anything, or hold and draw around an area, then write a note (Esc to stop)";
|
|
65
65
|
button.dataset.name = "note"; // names an agent's pen can point at
|
|
66
66
|
page.slot("status").prepend(button); // the first tool in the palette, whichever script loads first
|
|
@@ -79,8 +79,13 @@ export default async function (page) {
|
|
|
79
79
|
[...e.classList].slice(0, 2).map((c) => `.${c}`).join("");
|
|
80
80
|
|
|
81
81
|
function inMain(x, y) { // window px -> main-space px (the main space scrolls; card_show's place uses these)
|
|
82
|
-
const main = page.slot("top"), m = main.getBoundingClientRect();
|
|
83
|
-
return { x: Math.round(x - m.left + main.scrollLeft), y: Math.round(y - m.top + main.scrollTop) };
|
|
82
|
+
const main = page.slot("top"), m = main.getBoundingClientRect(), z = zoom(); // the board's zoom (board/browser.js)
|
|
83
|
+
return { x: Math.round((x - m.left) / z + main.scrollLeft), y: Math.round((y - m.top) / z + main.scrollTop) };
|
|
84
|
+
}
|
|
85
|
+
function zoom() { return parseFloat(page.slot("top").style.zoom) || 1; } // window px per main px (and per app-frame px)
|
|
86
|
+
function inFrame(fr, x, y) { // window px -> px in an app's frame (zoomed with the main space)
|
|
87
|
+
const r = fr.getBoundingClientRect(), z = zoom();
|
|
88
|
+
return [(x - r.left) / z - fr.clientLeft, (y - r.top) / z - fr.clientTop];
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
function target(x, y) {
|
|
@@ -89,9 +94,9 @@ export default async function (page) {
|
|
|
89
94
|
let inner = null, frame = null, box = el.getBoundingClientRect();
|
|
90
95
|
if (el.localName === "iframe") { // look inside the frame (an App): same origin, so its document is readable
|
|
91
96
|
try {
|
|
92
|
-
const r = el.getBoundingClientRect(), dx = r.left + el.clientLeft, dy = r.top + el.clientTop;
|
|
93
|
-
inner = el.contentDocument?.elementFromPoint(x
|
|
94
|
-
if (inner) { frame = el; const b = inner.getBoundingClientRect(); box = new DOMRect(b.x
|
|
97
|
+
const r = el.getBoundingClientRect(), z = zoom(), dx = r.left + el.clientLeft * z, dy = r.top + el.clientTop * z;
|
|
98
|
+
inner = el.contentDocument?.elementFromPoint(...inFrame(el, x, y));
|
|
99
|
+
if (inner) { frame = el; const b = inner.getBoundingClientRect(); box = new DOMRect(dx + b.x * z, dy + b.y * z, b.width * z, b.height * z); }
|
|
95
100
|
} catch {}
|
|
96
101
|
}
|
|
97
102
|
const hitEl = inner || el;
|
|
@@ -110,8 +115,7 @@ export default async function (page) {
|
|
|
110
115
|
let part = null; // what the thing itself says is at this spot (partAt), e.g. {"name": "enemy-2"}
|
|
111
116
|
try {
|
|
112
117
|
if (frame?.contentWindow?.partAt) {
|
|
113
|
-
|
|
114
|
-
part = frame.contentWindow.partAt(x - r.left - frame.clientLeft, y - r.top - frame.clientTop);
|
|
118
|
+
part = frame.contentWindow.partAt(...inFrame(frame, x, y));
|
|
115
119
|
} else {
|
|
116
120
|
const owner = [hitEl, ...[...document.elementsFromPoint(x, y)]].find((e) => typeof e?.partAt === "function");
|
|
117
121
|
if (owner) { const r = owner.getBoundingClientRect(); part = owner.partAt(x - r.left, y - r.top); }
|
|
@@ -188,8 +192,7 @@ export default async function (page) {
|
|
|
188
192
|
const el = document.elementsFromPoint(x, y).find((e) => e !== layer && e !== ink);
|
|
189
193
|
if (el?.localName === "iframe") {
|
|
190
194
|
try {
|
|
191
|
-
|
|
192
|
-
if (typeof el.contentWindow.partIn === "function") return await el.contentWindow.partIn(points.map(([px, py]) => [px - dx, py - dy]));
|
|
195
|
+
if (typeof el.contentWindow.partIn === "function") return await el.contentWindow.partIn(points.map(([px, py]) => inFrame(el, px, py)));
|
|
193
196
|
} catch (err) { console.warn("partIn:", err); }
|
|
194
197
|
}
|
|
195
198
|
const owner = document.elementsFromPoint(x, y).find((e) => typeof e.partIn === "function");
|
|
@@ -204,7 +207,7 @@ export default async function (page) {
|
|
|
204
207
|
const left = Math.min(...xs), top = Math.min(...ys), width = Math.max(...xs) - left, height = Math.max(...ys) - top;
|
|
205
208
|
if (width < 12 && height < 12) return openNote(target(e.clientX, e.clientY), e.clientX, e.clientY);
|
|
206
209
|
const t = target(left + width / 2, top + height / 2); // what is under the middle of the drawing
|
|
207
|
-
if (t) t.area = { ...inMain(left, top), width: Math.round(width), height: Math.round(height) };
|
|
210
|
+
if (t) t.area = { ...inMain(left, top), width: Math.round(width / zoom()), height: Math.round(height / zoom()) };
|
|
208
211
|
const inside = t && (await partIn(left + width / 2, top + height / 2, points));
|
|
209
212
|
if (inside) t.part = inside;
|
|
210
213
|
openNote(t, e.clientX, e.clientY, points);
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
#f { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 30; width: min(720px, 64vw);
|
|
194
194
|
display: flex; align-items: center; gap: 8px; padding: 2px 0 6px; border-bottom: 1px solid rgba(0, 0, 0, 0.16); }
|
|
195
195
|
#f:focus-within { border-bottom-color: rgba(0, 0, 0, 0.4); }
|
|
196
|
-
#to { border: 0; background: none; font: inherit; font-size: 12px; color: var(--muted); }
|
|
196
|
+
#to { border: 0; background: none; font: inherit; font-size: 12px; color: var(--muted); outline: none; }
|
|
197
197
|
#text { flex: 1; border: 0; outline: none; background: none; font: inherit; font-size: 16.5px; color: var(--text); padding: 4px 0; }
|
|
198
198
|
#text::placeholder { color: var(--muted); }
|
|
199
199
|
/* Slash commands: the list above the input while a command's name is typed */
|
|
@@ -49,9 +49,9 @@ export default async function (page) {
|
|
|
49
49
|
await new Promise((ok) => setTimeout(ok, 400)); // it has slid away; a fresh frame from the shared tab
|
|
50
50
|
const full = frame();
|
|
51
51
|
const s = full && await picture(full, { x: 0, y: 0, w: innerWidth, h: innerHeight }).catch(() => null);
|
|
52
|
-
const m = area.getBoundingClientRect(); // window (0, 0) in main-space px (as note/browser.js)
|
|
52
|
+
const m = area.getBoundingClientRect(), zoom = parseFloat(area.style.zoom) || 1; // window (0, 0) in main-space px (as note/browser.js)
|
|
53
53
|
page.send({ type: "screen_reply", id, shot: s?.path || null, window: { width: innerWidth, height: innerHeight },
|
|
54
|
-
main: { x: area.scrollLeft - m.left, y: area.scrollTop - m.top } });
|
|
54
|
+
main: { x: area.scrollLeft - m.left / zoom, y: area.scrollTop - m.top / zoom }, zoom });
|
|
55
55
|
}
|
|
56
56
|
page.on("screen_ask", ({ id, agent }) => {
|
|
57
57
|
if (shared()) return answer(id);
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// (their size, scroll, panels, running apps; browser-check's headless Chrome is a fresh window of its own).
|
|
5
5
|
// POST /screen/shot a png (at most MAX_BYTES): saved in data/screen, {path} back
|
|
6
6
|
// event "screen_ask" {id, agent} to every open page, when an agent calls screen_see
|
|
7
|
-
// page message screen_reply {id, shot, window: {width, height}, main: {x, y}}: the first answers
|
|
8
|
-
// (shot null: the user said no; main: window (0, 0) in main-space px
|
|
7
|
+
// page message screen_reply {id, shot, window: {width, height}, main: {x, y}, zoom}: the first answers
|
|
8
|
+
// (shot null: the user said no; main: window (0, 0) in main-space px; zoom: the board's,
|
|
9
|
+
// window px per main-space px)
|
|
9
10
|
// event "screen_ask_done" {id} to every page when answered or after ASK_WAIT s, so the asks go away
|
|
10
11
|
// host.api.screen isShot(path): whether a path is a picture saved here (others attach pictures by path)
|
|
11
12
|
import fs from "node:fs";
|
|
@@ -70,11 +71,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
70
71
|
if (!reply) return result(`no answer within ${ASK_WAIT} s (the page may not be open, or the user is away): ask them what they see`);
|
|
71
72
|
if (!isShot(reply.shot)) return result("the user didn't share their screen: ask them what they see");
|
|
72
73
|
const png = fs.readFileSync(reply.shot), pw = png.readUInt32BE(16), ph = png.readUInt32BE(20); // the png's own size (IHDR)
|
|
73
|
-
const w = reply.window ?? {}, m = reply.main ?? {}, k = w.width ? pw / w.width : 1;
|
|
74
|
+
const w = reply.window ?? {}, m = reply.main ?? {}, z = Number(reply.zoom) || 1, k = (w.width ? pw / w.width : 1) * z; // picture px per main px
|
|
74
75
|
const mx = Math.round(m.x ?? 0), my = Math.round(m.y ?? 0);
|
|
75
76
|
return result(`the user's screen right now: their window is ${w.width}x${w.height} px; the picture is the whole window, `
|
|
76
|
-
+ `${pw}x${ph} px (${+k.toFixed(3)} picture px per
|
|
77
|
-
+ `the picture is main-space x = ${mx} + picture x / ${+k.toFixed(3)},
|
|
77
|
+
+ `${pw}x${ph} px${z !== 1 ? `, with the board zoomed to ${Math.round(z * 100)}%` : ""} (${+k.toFixed(3)} picture px per main-space px). `
|
|
78
|
+
+ `Window (0, 0) is main-space (${mx}, ${my}): a spot in the picture is main-space x = ${mx} + picture x / ${+k.toFixed(3)}, `
|
|
79
|
+
+ `the same for y (what card_show's place uses).`, reply.shot);
|
|
78
80
|
},
|
|
79
81
|
});
|
|
80
82
|
}
|
|
@@ -15,15 +15,15 @@ const image = (src) => new Promise((ok, no) => Object.assign(new Image(), { onlo
|
|
|
15
15
|
async function picture() { // a PNG blob of the cards, or null if there are none
|
|
16
16
|
const top = document.getElementById("top"), cards = [...top.querySelectorAll("[data-card]")];
|
|
17
17
|
if (!cards.length) return null;
|
|
18
|
-
const domtoimage = await library(), at = top.getBoundingClientRect();
|
|
19
|
-
const box = (el) => { const r = el.getBoundingClientRect(); return { x: r.left - at.left + top.scrollLeft, y: r.top - at.top + top.scrollTop, w: r.width, h: r.height }; };
|
|
18
|
+
const domtoimage = await library(), at = top.getBoundingClientRect(), z = parseFloat(top.style.zoom) || 1; // the board's zoom: drawn at 100%
|
|
19
|
+
const box = (el) => { const r = el.getBoundingClientRect(); return { x: (r.left - at.left) / z + top.scrollLeft, y: (r.top - at.top) / z + top.scrollTop, w: r.width / z, h: r.height / z }; };
|
|
20
20
|
const all = cards.map(box);
|
|
21
21
|
const x0 = Math.max(0, Math.min(...all.map((b) => b.x)) - MARGIN), y0 = Math.max(0, Math.min(...all.map((b) => b.y)) - MARGIN);
|
|
22
22
|
const x1 = Math.max(...all.map((b) => b.x + b.w)) + MARGIN, y1 = Math.max(...all.map((b) => b.y + b.h)) + MARGIN;
|
|
23
23
|
const width = Math.ceil(x1), height = Math.ceil(y1); // the whole main space up to the last card; cropped below
|
|
24
24
|
const bgcolor = getComputedStyle(document.body).backgroundColor;
|
|
25
25
|
const page = await image(await domtoimage.toPng(top, { width, height, bgcolor,
|
|
26
|
-
style: { position: "static", inset: "auto", overflow: "visible", width: `${width}px`, height: `${height}px
|
|
26
|
+
style: { position: "static", inset: "auto", overflow: "visible", width: `${width}px`, height: `${height}px`, zoom: "1" } }));
|
|
27
27
|
const canvas = Object.assign(document.createElement("canvas"), { width: Math.ceil(x1 - x0), height: Math.ceil(y1 - y0) });
|
|
28
28
|
const g = canvas.getContext("2d");
|
|
29
29
|
g.drawImage(page, -x0, -y0);
|