@nuniapp/widget 0.1.8 → 0.1.9
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/dom-BWcT-q7S.js +255 -0
- package/dist/index.d.ts +87 -6
- package/dist/index.js +606 -150
- package/dist/nuni-screenshot.global.js +41 -7
- package/dist/nuni.global.js +91 -8
- package/dist/{screenshot-DcJ_SzKF.js → tools-gIAPu66h.js} +225 -14
- package/package.json +1 -1
- package/dist/mark-B8wojiFu.js +0 -90
|
@@ -1,4 +1,215 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { d as CAPTURE_MARK, f as MASK_ATTRIBUTE, l as encodeCanvas, m as LIMITS, r as h } from "./dom-BWcT-q7S.js";
|
|
2
|
+
//#region src/markup.ts
|
|
3
|
+
const STYLES = `
|
|
4
|
+
.mk {
|
|
5
|
+
position: fixed;
|
|
6
|
+
inset: 0;
|
|
7
|
+
display: grid;
|
|
8
|
+
place-items: center;
|
|
9
|
+
padding: 16px;
|
|
10
|
+
background: rgb(0 0 0 / 0.55);
|
|
11
|
+
pointer-events: auto;
|
|
12
|
+
z-index: 10;
|
|
13
|
+
}
|
|
14
|
+
.mk-box {
|
|
15
|
+
display: grid;
|
|
16
|
+
gap: 10px;
|
|
17
|
+
max-width: 100%;
|
|
18
|
+
padding: 10px;
|
|
19
|
+
border-radius: 14px;
|
|
20
|
+
background: var(--n-bg);
|
|
21
|
+
color: var(--n-fg);
|
|
22
|
+
box-shadow: var(--n-shadow);
|
|
23
|
+
}
|
|
24
|
+
.mk-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
|
25
|
+
.mk-bar .spacer { flex: 1; }
|
|
26
|
+
.mk-error { margin: 0; color: var(--n-danger); font-size: 13px; }
|
|
27
|
+
.mk-error:empty { display: none; }
|
|
28
|
+
.mk-bar [aria-pressed="true"] { background: var(--n-accent-soft); border-color: var(--n-accent); }
|
|
29
|
+
.mk-canvas {
|
|
30
|
+
display: block;
|
|
31
|
+
max-width: min(960px, calc(100vw - 52px));
|
|
32
|
+
max-height: calc(100vh - 120px);
|
|
33
|
+
border-radius: 8px;
|
|
34
|
+
cursor: crosshair;
|
|
35
|
+
touch-action: none;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
function lineWidth(canvas) {
|
|
39
|
+
return Math.max(3, Math.round(Math.max(canvas.width, canvas.height) / 250));
|
|
40
|
+
}
|
|
41
|
+
function drawShape(ctx, shape, width) {
|
|
42
|
+
const [a, b] = shape.points;
|
|
43
|
+
if (!a) return;
|
|
44
|
+
ctx.beginPath();
|
|
45
|
+
if (shape.tool === "pen") {
|
|
46
|
+
ctx.moveTo(a.x, a.y);
|
|
47
|
+
for (const p of shape.points.slice(1)) ctx.lineTo(p.x, p.y);
|
|
48
|
+
} else if (b && shape.tool === "box") ctx.rect(a.x, a.y, b.x - a.x, b.y - a.y);
|
|
49
|
+
else if (b) {
|
|
50
|
+
const angle = Math.atan2(b.y - a.y, b.x - a.x);
|
|
51
|
+
const head = width * 4;
|
|
52
|
+
ctx.moveTo(a.x, a.y);
|
|
53
|
+
ctx.lineTo(b.x, b.y);
|
|
54
|
+
for (const side of [-1, 1]) {
|
|
55
|
+
ctx.moveTo(b.x, b.y);
|
|
56
|
+
ctx.lineTo(b.x - head * Math.cos(angle + side * Math.PI / 7), b.y - head * Math.sin(angle + side * Math.PI / 7));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
ctx.stroke();
|
|
60
|
+
}
|
|
61
|
+
/** Big enough to be meant, not a stray click. */
|
|
62
|
+
function isDrawn(shape) {
|
|
63
|
+
const [a, b] = shape.points;
|
|
64
|
+
if (!a || !b) return false;
|
|
65
|
+
if (shape.tool === "pen") return shape.points.length > 2;
|
|
66
|
+
return Math.hypot(b.x - a.x, b.y - a.y) > 6;
|
|
67
|
+
}
|
|
68
|
+
const markUp = async (image, { container, labels, color }) => {
|
|
69
|
+
let bitmap;
|
|
70
|
+
try {
|
|
71
|
+
bitmap = await createImageBitmap(image);
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const root = container.getRootNode();
|
|
76
|
+
if (!root.querySelector("style[data-nuni-markup]")) {
|
|
77
|
+
const style = h("style", { "data-nuni-markup": "" }, STYLES);
|
|
78
|
+
(root instanceof Document ? root.head : root).append(style);
|
|
79
|
+
}
|
|
80
|
+
const canvas = h("canvas", {
|
|
81
|
+
class: "mk-canvas",
|
|
82
|
+
width: bitmap.width,
|
|
83
|
+
height: bitmap.height,
|
|
84
|
+
role: "img",
|
|
85
|
+
"aria-label": labels.title
|
|
86
|
+
});
|
|
87
|
+
const ctx = canvas.getContext("2d");
|
|
88
|
+
if (!ctx) {
|
|
89
|
+
bitmap.close();
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const width = lineWidth(canvas);
|
|
93
|
+
const shapes = [];
|
|
94
|
+
let current = null;
|
|
95
|
+
let tool = "box";
|
|
96
|
+
const draw = () => {
|
|
97
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
98
|
+
ctx.strokeStyle = color;
|
|
99
|
+
ctx.lineWidth = width;
|
|
100
|
+
ctx.lineCap = "round";
|
|
101
|
+
ctx.lineJoin = "round";
|
|
102
|
+
for (const shape of current ? [...shapes, current] : shapes) drawShape(ctx, shape, width);
|
|
103
|
+
undo.disabled = shapes.length === 0;
|
|
104
|
+
};
|
|
105
|
+
const point = (e) => {
|
|
106
|
+
const rect = canvas.getBoundingClientRect();
|
|
107
|
+
return {
|
|
108
|
+
x: (e.clientX - rect.left) / rect.width * canvas.width,
|
|
109
|
+
y: (e.clientY - rect.top) / rect.height * canvas.height
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
canvas.addEventListener("pointerdown", (e) => {
|
|
113
|
+
if (!e.isPrimary) return;
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
canvas.setPointerCapture(e.pointerId);
|
|
116
|
+
const p = point(e);
|
|
117
|
+
current = {
|
|
118
|
+
tool,
|
|
119
|
+
points: tool === "pen" ? [p] : [p, p]
|
|
120
|
+
};
|
|
121
|
+
draw();
|
|
122
|
+
});
|
|
123
|
+
canvas.addEventListener("pointermove", (e) => {
|
|
124
|
+
if (!current) return;
|
|
125
|
+
const p = point(e);
|
|
126
|
+
if (current.tool === "pen") current.points.push(p);
|
|
127
|
+
else current.points[1] = p;
|
|
128
|
+
draw();
|
|
129
|
+
});
|
|
130
|
+
const finish = () => {
|
|
131
|
+
if (current && isDrawn(current)) shapes.push(current);
|
|
132
|
+
current = null;
|
|
133
|
+
draw();
|
|
134
|
+
};
|
|
135
|
+
canvas.addEventListener("pointerup", finish);
|
|
136
|
+
canvas.addEventListener("pointercancel", finish);
|
|
137
|
+
const toolButtons = [
|
|
138
|
+
"box",
|
|
139
|
+
"arrow",
|
|
140
|
+
"pen"
|
|
141
|
+
].map((name) => h("button", {
|
|
142
|
+
class: "btn",
|
|
143
|
+
type: "button",
|
|
144
|
+
"aria-pressed": String(name === tool),
|
|
145
|
+
onclick: () => {
|
|
146
|
+
tool = name;
|
|
147
|
+
for (const b of toolButtons) b.setAttribute("aria-pressed", String(b.dataset.tool === tool));
|
|
148
|
+
},
|
|
149
|
+
"data-tool": name
|
|
150
|
+
}, labels[name]));
|
|
151
|
+
const undo = h("button", {
|
|
152
|
+
class: "btn btn-ghost",
|
|
153
|
+
type: "button",
|
|
154
|
+
onclick: () => {
|
|
155
|
+
shapes.pop();
|
|
156
|
+
draw();
|
|
157
|
+
}
|
|
158
|
+
}, labels.undo);
|
|
159
|
+
const previous = root.activeElement;
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
let closing = false;
|
|
162
|
+
const close = async (save) => {
|
|
163
|
+
if (closing) return;
|
|
164
|
+
closing = true;
|
|
165
|
+
const result = save ? await encodeCanvas(canvas, LIMITS.imageMaxBytes) : null;
|
|
166
|
+
if (save && !result) {
|
|
167
|
+
closing = false;
|
|
168
|
+
error.textContent = labels.failed;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
overlay.remove();
|
|
172
|
+
bitmap.close();
|
|
173
|
+
previous?.focus();
|
|
174
|
+
resolve(result);
|
|
175
|
+
};
|
|
176
|
+
const error = h("p", {
|
|
177
|
+
class: "mk-error",
|
|
178
|
+
role: "alert"
|
|
179
|
+
});
|
|
180
|
+
const done = h("button", {
|
|
181
|
+
class: "btn btn-primary",
|
|
182
|
+
type: "button",
|
|
183
|
+
onclick: () => void close(true)
|
|
184
|
+
}, labels.done);
|
|
185
|
+
const overlay = h("div", {
|
|
186
|
+
class: "mk",
|
|
187
|
+
role: "dialog",
|
|
188
|
+
"aria-modal": "true",
|
|
189
|
+
"aria-label": labels.title,
|
|
190
|
+
onkeydown: (e) => {
|
|
191
|
+
const key = e;
|
|
192
|
+
if (key.key === "Escape") close(false);
|
|
193
|
+
else if (key.key === "z" && (key.metaKey || key.ctrlKey)) {
|
|
194
|
+
shapes.pop();
|
|
195
|
+
draw();
|
|
196
|
+
} else return;
|
|
197
|
+
key.preventDefault();
|
|
198
|
+
}
|
|
199
|
+
}, h("div", { class: "mk-box" }, h("div", {
|
|
200
|
+
class: "mk-bar",
|
|
201
|
+
role: "toolbar",
|
|
202
|
+
"aria-label": labels.title
|
|
203
|
+
}, ...toolButtons, undo, h("span", { class: "spacer" }), h("button", {
|
|
204
|
+
class: "btn btn-ghost",
|
|
205
|
+
type: "button",
|
|
206
|
+
onclick: () => void close(false)
|
|
207
|
+
}, labels.cancel), done), error, canvas));
|
|
208
|
+
container.append(overlay);
|
|
209
|
+
draw();
|
|
210
|
+
done.focus();
|
|
211
|
+
});
|
|
212
|
+
};
|
|
2
213
|
"p".charCodeAt(0);
|
|
3
214
|
"H".charCodeAt(0);
|
|
4
215
|
"Y".charCodeAt(0);
|
|
@@ -1223,9 +1434,6 @@ function maskedBoxes(frame) {
|
|
|
1223
1434
|
}
|
|
1224
1435
|
return out;
|
|
1225
1436
|
}
|
|
1226
|
-
function toBlob(canvas, type, quality) {
|
|
1227
|
-
return new Promise((resolve) => canvas.toBlob(resolve, type, quality));
|
|
1228
|
-
}
|
|
1229
1437
|
const captureScreenshot = async (el, options) => {
|
|
1230
1438
|
const frame = frameFor(el);
|
|
1231
1439
|
const { box, target, masks, crop } = isolatedLayout(frame, el, options.focus);
|
|
@@ -1274,15 +1482,18 @@ const captureScreenshot = async (el, options) => {
|
|
|
1274
1482
|
ctx.strokeStyle = options.accent;
|
|
1275
1483
|
ctx.lineWidth = line;
|
|
1276
1484
|
if (x1 > x0 && y1 > y0) ctx.strokeRect(x0, y0, x1 - x0, y1 - y0);
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1485
|
+
return encodeCanvas(canvas, LIMITS.screenshotMaxBytes);
|
|
1486
|
+
};
|
|
1487
|
+
//#endregion
|
|
1488
|
+
//#region src/tools.ts
|
|
1489
|
+
/**
|
|
1490
|
+
* The code loaded on demand once someone comments (a separate chunk for npm
|
|
1491
|
+
* users, a separate script for the CDN build): taking the screenshot and
|
|
1492
|
+
* marking it up.
|
|
1493
|
+
*/
|
|
1494
|
+
const screenshotTools = {
|
|
1495
|
+
capture: captureScreenshot,
|
|
1496
|
+
markUp
|
|
1286
1497
|
};
|
|
1287
1498
|
//#endregion
|
|
1288
|
-
export {
|
|
1499
|
+
export { screenshotTools };
|
package/package.json
CHANGED
package/dist/mark-B8wojiFu.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
//#region ../shared/src/id.ts
|
|
2
|
-
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
3
|
-
const PROJECT_ID_PATTERN = new RegExp(`^nuni_[${ALPHABET}]{22}$`);
|
|
4
|
-
/** Random base58 string using the platform CSPRNG (browser, Node, Convex). */
|
|
5
|
-
function randomBase58(length) {
|
|
6
|
-
const bytes = new Uint8Array(length);
|
|
7
|
-
crypto.getRandomValues(bytes);
|
|
8
|
-
let out = "";
|
|
9
|
-
for (const byte of bytes) {
|
|
10
|
-
let b = byte;
|
|
11
|
-
while (b >= 232) {
|
|
12
|
-
const retry = /* @__PURE__ */ new Uint8Array(1);
|
|
13
|
-
crypto.getRandomValues(retry);
|
|
14
|
-
b = retry[0];
|
|
15
|
-
}
|
|
16
|
-
out += ALPHABET[b % 58];
|
|
17
|
-
}
|
|
18
|
-
return out;
|
|
19
|
-
}
|
|
20
|
-
function isProjectId(value) {
|
|
21
|
-
return typeof value === "string" && PROJECT_ID_PATTERN.test(value);
|
|
22
|
-
}
|
|
23
|
-
/** Secret used by anonymous commenters to edit/delete their own comments. */
|
|
24
|
-
function generateSecret() {
|
|
25
|
-
return randomBase58(32);
|
|
26
|
-
}
|
|
27
|
-
//#endregion
|
|
28
|
-
//#region ../shared/src/limits.ts
|
|
29
|
-
const LIMITS = {
|
|
30
|
-
bodyMaxLength: 2e3,
|
|
31
|
-
nameMaxLength: 50,
|
|
32
|
-
unclaimedCommentCap: 500,
|
|
33
|
-
maxOriginsPerProject: 20,
|
|
34
|
-
sessionTtlMs: 2592e6,
|
|
35
|
-
/** CLI and MCP sessions, created by `nuni login`. */
|
|
36
|
-
cliSessionTtlMs: 7776e6,
|
|
37
|
-
/** How long a project transfer link works. */
|
|
38
|
-
transferTtlMs: 6048e5,
|
|
39
|
-
/** Comments changed at once from the dashboard. */
|
|
40
|
-
bulkMax: 100,
|
|
41
|
-
/** How long a `nuni login` code waits for approval in the dashboard. */
|
|
42
|
-
cliLoginTtlMs: 6e5,
|
|
43
|
-
anchorTextMaxLength: 120,
|
|
44
|
-
/** Selected text kept with a text comment, and the context around it. */
|
|
45
|
-
quoteMaxLength: 500,
|
|
46
|
-
quoteContextLength: 32,
|
|
47
|
-
/** Per page, the widget loads this many newest open and resolved comments. */
|
|
48
|
-
pageOpenLimit: 1e3,
|
|
49
|
-
pageResolvedLimit: 200,
|
|
50
|
-
/** Most console or network entries kept with one comment. */
|
|
51
|
-
contextEntryMax: 20,
|
|
52
|
-
contextMessageMaxLength: 500,
|
|
53
|
-
/** Trimmed outerHTML of the commented element. */
|
|
54
|
-
domSnippetMaxLength: 4e3,
|
|
55
|
-
screenshotMaxBytes: 6e5,
|
|
56
|
-
/** Replies kept per comment thread. */
|
|
57
|
-
repliesPerComment: 200,
|
|
58
|
-
/** Reactions kept per thread (the comment and its replies). */
|
|
59
|
-
reactionsPerComment: 2e3,
|
|
60
|
-
/** How long after posting the author can still attach the screenshot. */
|
|
61
|
-
screenshotUploadWindowMs: 3e5
|
|
62
|
-
};
|
|
63
|
-
/** The reactions anyone can add to a comment or reply. */
|
|
64
|
-
const REACTIONS = [
|
|
65
|
-
"👍",
|
|
66
|
-
"❤️",
|
|
67
|
-
"🎉",
|
|
68
|
-
"👀",
|
|
69
|
-
"✅",
|
|
70
|
-
"😄"
|
|
71
|
-
];
|
|
72
|
-
//#endregion
|
|
73
|
-
//#region ../shared/src/privacy.ts
|
|
74
|
-
/**
|
|
75
|
-
* Site owners mark private areas with this attribute. Their text never
|
|
76
|
-
* reaches Nuni: not in the pin anchor (which is public), the markup snippet
|
|
77
|
-
* or the screenshot.
|
|
78
|
-
*/
|
|
79
|
-
const MASK_ATTRIBUTE = "data-nuni-mask";
|
|
80
|
-
const MASK_SELECTOR = `[${MASK_ATTRIBUTE}]`;
|
|
81
|
-
//#endregion
|
|
82
|
-
//#region src/mark.ts
|
|
83
|
-
/**
|
|
84
|
-
* Marks Nuni's own screenshot requests (fonts and images it inlines), so the
|
|
85
|
-
* network collector can skip them without muting the page. `Symbol.for` is
|
|
86
|
-
* shared by the widget and the separately loaded screenshot script.
|
|
87
|
-
*/
|
|
88
|
-
const CAPTURE_MARK = Symbol.for("nuni.capture");
|
|
89
|
-
//#endregion
|
|
90
|
-
export { REACTIONS as a, LIMITS as i, MASK_ATTRIBUTE as n, generateSecret as o, MASK_SELECTOR as r, isProjectId as s, CAPTURE_MARK as t };
|