mesurer 0.2.6 → 0.2.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/CHANGELOG.md +6 -0
- package/README.md +2 -2
- package/dist/chunk-R2UAUDQP.js +345 -0
- package/dist/ephemeral-url-VBGGJ7MW.js +8 -0
- package/dist/index.cjs +3713 -1506
- package/dist/index.d.cts +273 -27
- package/dist/index.d.ts +273 -27
- package/dist/index.js +2677 -1112
- package/dist/loading-page-6X6HK52V.js +38 -0
- package/dist/snapdom-ISDYYTQF.js +136 -0
- package/dist/styles.css +1 -1
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# mesurer
|
|
2
2
|
|
|
3
|
+
## 0.2.7
|
|
4
|
+
|
|
5
|
+
- add a pens tray to the pen tool: pick ink or marker, set each one's width and colour, and find each as it was left
|
|
6
|
+
- add an eraser to the pen tool that rubs out only the part of a stroke it passes over
|
|
7
|
+
- make ink strokes smooth, with a width that changes with how fast or hard you draw
|
|
8
|
+
|
|
3
9
|
## 0.2.6
|
|
4
10
|
|
|
5
11
|
- add responsive previews (`B`): the page at several screen sizes side by side, with Inspect, Comments, Guides, and X-ray working in each
|
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ function App() {
|
|
|
70
70
|
| `multiMeasureEnabled` | Keep previous measurements visible. Default `false`. |
|
|
71
71
|
| `guideStyle` | Guide opacity, width, pattern, dash length, and gap. |
|
|
72
72
|
| `rulerSettings` | Ruler opacity and edge reveal. |
|
|
73
|
-
| `textStyle` |
|
|
73
|
+
| `textStyle` | Font, color, and background new text notes start with. |
|
|
74
74
|
|
|
75
75
|
Props are the defaults. Saved settings override them; **Use defaults** restores the props.
|
|
76
76
|
|
|
@@ -128,7 +128,7 @@ Props are the defaults. Saved settings override them; **Use defaults** restores
|
|
|
128
128
|
- **Distance overlays** - Hold Alt for quick spacing checks
|
|
129
129
|
- **Undo/redo** - Command history for guides, measurements, arrows, pen strokes, and text annotations
|
|
130
130
|
- **Comments** - Pin threads to live elements, add replies, move targets, search threads, and copy structured feedback for agents
|
|
131
|
-
- **Settings** - Configure selection, guide, ruler, arrow,
|
|
131
|
+
- **Settings** - Configure selection, guide, ruler, arrow, color, format, and persistence behavior
|
|
132
132
|
- **Workspace controls** - Persist page workspaces, restore defaults, clear annotations, and minimize Mesurer to a single toolbar button
|
|
133
133
|
|
|
134
134
|
## Requirements
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// core/review/ephemeral-url.ts
|
|
2
|
+
var bytesToBase64 = (bytes) => {
|
|
3
|
+
let binary = "";
|
|
4
|
+
const chunkSize = 32768;
|
|
5
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
6
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));
|
|
7
|
+
}
|
|
8
|
+
return btoa(binary);
|
|
9
|
+
};
|
|
10
|
+
var blobToDataUrl = async (blob) => {
|
|
11
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
12
|
+
return `data:${blob.type || "image/png"};base64,${bytesToBase64(bytes)}`;
|
|
13
|
+
};
|
|
14
|
+
var encodePayload = (payload) => bytesToBase64(new TextEncoder().encode(JSON.stringify(payload)));
|
|
15
|
+
var transformAnnotation = (annotation, origin, scale) => {
|
|
16
|
+
const point = (value) => ({ x: (value.x - origin.x) * scale, y: (value.y - origin.y) * scale });
|
|
17
|
+
const rect = (value) => ({
|
|
18
|
+
left: (value.left - origin.x) * scale,
|
|
19
|
+
top: (value.top - origin.y) * scale,
|
|
20
|
+
width: value.width * scale,
|
|
21
|
+
height: value.height * scale
|
|
22
|
+
});
|
|
23
|
+
if (annotation.type === "arrow") return { ...annotation, start: point(annotation.start), end: point(annotation.end), ...annotation.control ? { control: point(annotation.control) } : {} };
|
|
24
|
+
if (annotation.type === "pen") return { ...annotation, points: annotation.points.map(point) };
|
|
25
|
+
if (annotation.type === "rectangle") return { ...annotation, rect: rect(annotation.rect) };
|
|
26
|
+
if (annotation.type === "text") return { ...annotation, x: (annotation.x - origin.x) * scale, y: (annotation.y - origin.y) * scale, ...annotation.boxWidth ? { boxWidth: annotation.boxWidth * scale } : {} };
|
|
27
|
+
if (annotation.type === "guide") return { ...annotation, position: (annotation.position - (annotation.orientation === "vertical" ? origin.x : origin.y)) * scale };
|
|
28
|
+
if (annotation.kind === "element") return { ...annotation, rect: rect(annotation.rect) };
|
|
29
|
+
return {
|
|
30
|
+
...annotation,
|
|
31
|
+
rectA: rect(annotation.rectA),
|
|
32
|
+
rectB: rect(annotation.rectB),
|
|
33
|
+
connectors: annotation.connectors.map((connector) => ({ ...connector, x1: (connector.x1 - origin.x) * scale, x2: (connector.x2 - origin.x) * scale, y1: (connector.y1 - origin.y) * scale, y2: (connector.y2 - origin.y) * scale })),
|
|
34
|
+
...annotation.horizontal ? { horizontal: { ...annotation.horizontal, x1: (annotation.horizontal.x1 - origin.x) * scale, x2: (annotation.horizontal.x2 - origin.x) * scale, y: (annotation.horizontal.y - origin.y) * scale } } : {},
|
|
35
|
+
...annotation.vertical ? { vertical: { ...annotation.vertical, x: (annotation.vertical.x - origin.x) * scale, y1: (annotation.vertical.y1 - origin.y) * scale, y2: (annotation.vertical.y2 - origin.y) * scale } } : {}
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
var createViewerHtml = (encodedPayload) => `<!doctype html>
|
|
39
|
+
<html lang="en">
|
|
40
|
+
<head>
|
|
41
|
+
<meta charset="utf-8">
|
|
42
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
43
|
+
<title>Mesurer review</title>
|
|
44
|
+
<style>
|
|
45
|
+
:root { color-scheme: light; font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
46
|
+
* { box-sizing: border-box; }
|
|
47
|
+
body { margin: 0; background: #f4f4f5; color: #18181b; }
|
|
48
|
+
header { position: sticky; top: 0; z-index: 2; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 16px; background: rgba(255,255,255,.94); border-bottom: 1px solid #e4e4e7; backdrop-filter: blur(10px); }
|
|
49
|
+
header strong { font-size: 14px; }
|
|
50
|
+
header span { color: #71717a; font-size: 12px; }
|
|
51
|
+
main { padding: 24px; }
|
|
52
|
+
.stage { position: relative; width: min(100%, 1440px); margin: 0 auto; overflow: hidden; background: white; box-shadow: 0 12px 40px rgba(24,24,27,.12); }
|
|
53
|
+
.stage img { display: block; width: 100%; height: auto; }
|
|
54
|
+
.stage .live { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: white; pointer-events: none; }
|
|
55
|
+
.annotations { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
|
|
56
|
+
.layout-guides { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
|
|
57
|
+
.layout-guide { position: absolute; display: grid; pointer-events: none; }
|
|
58
|
+
.layout-guide > i { display: block; background: var(--guide-color); opacity: var(--guide-opacity); }
|
|
59
|
+
.text-annotations { position: absolute; inset: 0; pointer-events: none; }
|
|
60
|
+
.annotation-text { position: absolute; white-space: pre-wrap; pointer-events: none; transform-origin: center center; }
|
|
61
|
+
.comments { position: absolute; inset: 0; pointer-events: none; }
|
|
62
|
+
.comment-pin { position: absolute; width: 24px; height: 24px; transform: translate(-50%, -50%); border: 2px solid white; border-radius: 999px; background: #0d99ff; color: white; box-shadow: 0 2px 8px rgba(0,0,0,.25); cursor: pointer; pointer-events: auto; font: 700 11px ui-monospace, monospace; }
|
|
63
|
+
.comment-card { position: absolute; z-index: 1; width: 240px; padding: 10px; border: 1px solid #d4d4d8; border-radius: 8px; background: white; box-shadow: 0 8px 24px rgba(24,24,27,.16); pointer-events: auto; font-size: 12px; }
|
|
64
|
+
.comment-card[hidden] { display: none; }
|
|
65
|
+
.comment-card strong { display: block; margin-bottom: 6px; }
|
|
66
|
+
.comment-card p { margin: 0 0 8px; white-space: pre-wrap; }
|
|
67
|
+
.comment-card small { color: #71717a; }
|
|
68
|
+
.comment-message { margin-top: 10px; }
|
|
69
|
+
.comment-message header { position: static; padding: 0; border: 0; background: transparent; font-size: 11px; }
|
|
70
|
+
.comment-reply { display: flex; gap: 6px; margin-top: 12px; }
|
|
71
|
+
.comment-reply textarea { min-width: 0; flex: 1; resize: vertical; border: 1px solid #d4d4d8; border-radius: 6px; padding: 6px; font: inherit; font-size: 12px; }
|
|
72
|
+
.comment-reply button { align-self: end; border: 0; border-radius: 6px; padding: 7px 9px; background: #18181b; color: white; cursor: pointer; font-size: 11px; }
|
|
73
|
+
details { width: min(100%, 1440px); margin: 16px auto 0; padding: 12px 16px; border: 1px solid #e4e4e7; border-radius: 8px; background: white; }
|
|
74
|
+
summary { cursor: pointer; font-size: 13px; font-weight: 600; }
|
|
75
|
+
pre { max-height: 480px; overflow: auto; white-space: pre-wrap; font: 11px/1.5 ui-monospace, monospace; }
|
|
76
|
+
</style>
|
|
77
|
+
</head>
|
|
78
|
+
<body>
|
|
79
|
+
<header><strong id="title">Mesurer review</strong><span id="meta"></span></header>
|
|
80
|
+
<main>
|
|
81
|
+
<section class="stage" id="stage"><img id="screenshot" alt="Captured Mesurer review snapshot"><div class="layout-guides" id="layout-guides"></div><svg class="annotations" id="annotations" aria-label="Review annotations"></svg><div class="text-annotations" id="text-annotations"></div><div class="comments" id="comments"></div></section>
|
|
82
|
+
<details><summary>Review state</summary><pre id="state"></pre></details>
|
|
83
|
+
</main>
|
|
84
|
+
<script>
|
|
85
|
+
(() => {
|
|
86
|
+
const decode = (value) => {
|
|
87
|
+
const binary = atob(value);
|
|
88
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
89
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
90
|
+
};
|
|
91
|
+
const payload = decode(${JSON.stringify(encodedPayload)});
|
|
92
|
+
const state = payload.state;
|
|
93
|
+
const version = state.versions[state.versions.length - 1];
|
|
94
|
+
const snapshot = version.snapshot;
|
|
95
|
+
const screenshot = document.getElementById("screenshot");
|
|
96
|
+
const annotations = document.getElementById("annotations");
|
|
97
|
+
const layoutGuides = document.getElementById("layout-guides");
|
|
98
|
+
const textAnnotations = document.getElementById("text-annotations");
|
|
99
|
+
const comments = document.getElementById("comments");
|
|
100
|
+
const title = document.getElementById("title");
|
|
101
|
+
const meta = document.getElementById("meta");
|
|
102
|
+
const stateOutput = document.getElementById("state");
|
|
103
|
+
title.textContent = state.page.title || state.page.url;
|
|
104
|
+
meta.textContent = "Version " + version.number + " \xB7 " + state.review.comments.length + " comments \xB7 " + state.review.annotations.length + " annotations";
|
|
105
|
+
screenshot.src = payload.screenshotDataUrl;
|
|
106
|
+
if (payload.liveSnapshot) {
|
|
107
|
+
// Over the screenshot, which keeps the stage's size and shows through if this cannot load.
|
|
108
|
+
const live = document.createElement("iframe");
|
|
109
|
+
live.className = "live";
|
|
110
|
+
live.id = "live-snapshot";
|
|
111
|
+
live.title = "Captured page";
|
|
112
|
+
live.setAttribute("sandbox", "");
|
|
113
|
+
live.src = URL.createObjectURL(new Blob([payload.liveSnapshot], { type: "image/svg+xml" }));
|
|
114
|
+
screenshot.after(live);
|
|
115
|
+
}
|
|
116
|
+
stateOutput.textContent = JSON.stringify(state, null, 2);
|
|
117
|
+
const width = snapshot.viewport.width || 1;
|
|
118
|
+
const height = snapshot.viewport.height || 1;
|
|
119
|
+
annotations.setAttribute("viewBox", "0 0 " + width + " " + height);
|
|
120
|
+
(state.review.workspace?.layoutGuides || []).filter((guide) => guide.visible).forEach((guide) => {
|
|
121
|
+
const element = document.createElement("div");
|
|
122
|
+
element.className = "layout-guide";
|
|
123
|
+
element.style.setProperty("--guide-color", guide.color);
|
|
124
|
+
element.style.setProperty("--guide-opacity", String(guide.opacity));
|
|
125
|
+
const horizontal = guide.kind === "rows";
|
|
126
|
+
const grid = guide.kind === "grid";
|
|
127
|
+
const size = Math.max(0, guide.size);
|
|
128
|
+
const dimension = horizontal ? "height" : "width";
|
|
129
|
+
if (guide.align !== "stretch" && !grid) {
|
|
130
|
+
element.style[dimension] = Math.min(size, horizontal ? height : width) + "px";
|
|
131
|
+
element.style[horizontal ? "width" : "height"] = "100%";
|
|
132
|
+
element.style[horizontal ? "left" : "top"] = "0";
|
|
133
|
+
element.style[horizontal ? "top" : "left"] = guide.align === "center"
|
|
134
|
+
? "calc(50% - " + Math.min(size, horizontal ? height : width) / 2 + "px)"
|
|
135
|
+
: guide.align === "max"
|
|
136
|
+
? "calc(100% - " + Math.min(size, horizontal ? height : width) + "px)"
|
|
137
|
+
: "0";
|
|
138
|
+
} else {
|
|
139
|
+
element.style.inset = "0";
|
|
140
|
+
}
|
|
141
|
+
element.style.gap = guide.gutter + "px";
|
|
142
|
+
if (grid) {
|
|
143
|
+
element.style.gridTemplateColumns = "repeat(" + guide.count + ", minmax(0, 1fr))";
|
|
144
|
+
element.style.gridTemplateRows = "repeat(" + guide.count + ", minmax(0, 1fr))";
|
|
145
|
+
} else if (horizontal) {
|
|
146
|
+
element.style.gridTemplateRows = "repeat(" + guide.count + ", minmax(0, 1fr))";
|
|
147
|
+
element.style.gridTemplateColumns = "1fr";
|
|
148
|
+
} else {
|
|
149
|
+
element.style.gridTemplateColumns = "repeat(" + guide.count + ", minmax(0, 1fr))";
|
|
150
|
+
element.style.gridTemplateRows = "1fr";
|
|
151
|
+
}
|
|
152
|
+
const cells = grid ? guide.count * guide.count : guide.count;
|
|
153
|
+
for (let index = 0; index < cells; index += 1) element.append(document.createElement("i"));
|
|
154
|
+
layoutGuides.append(element);
|
|
155
|
+
});
|
|
156
|
+
const svg = (name, attributes) => {
|
|
157
|
+
const element = document.createElementNS("http://www.w3.org/2000/svg", name);
|
|
158
|
+
Object.entries(attributes).forEach(([key, value]) => element.setAttribute(key, String(value)));
|
|
159
|
+
return element;
|
|
160
|
+
};
|
|
161
|
+
const line = (x1, y1, x2, y2, color, strokeWidth, dash) => {
|
|
162
|
+
annotations.append(svg("line", { x1, y1, x2, y2, stroke: color || "#0d99ff", "stroke-width": strokeWidth || 2, "stroke-dasharray": dash || "" }));
|
|
163
|
+
};
|
|
164
|
+
const midpoint = (start, end) => ({ x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 });
|
|
165
|
+
const quadraticPoint = (start, node, end, t) => {
|
|
166
|
+
const control = { x: 2 * node.x - (start.x + end.x) / 2, y: 2 * node.y - (start.y + end.y) / 2 };
|
|
167
|
+
const mt = 1 - t;
|
|
168
|
+
return { x: mt * mt * start.x + 2 * mt * t * control.x + t * t * end.x, y: mt * mt * start.y + 2 * mt * t * control.y + t * t * end.y };
|
|
169
|
+
};
|
|
170
|
+
const arrowHead = (start, node, end, width) => {
|
|
171
|
+
const from = quadraticPoint(start, node, end, 0.82);
|
|
172
|
+
const length = Math.max(12, width * 8);
|
|
173
|
+
const tangentLength = Math.hypot(end.x - from.x, end.y - from.y) || 1;
|
|
174
|
+
const tx = (end.x - from.x) / tangentLength;
|
|
175
|
+
const ty = (end.y - from.y) / tangentLength;
|
|
176
|
+
const wing = length * 0.5;
|
|
177
|
+
const nx = -ty;
|
|
178
|
+
const ny = tx;
|
|
179
|
+
return (end.x - tx * length + nx * wing) + "," + (end.y - ty * length + ny * wing) + " " + end.x + "," + end.y + " " + (end.x - tx * length - nx * wing) + "," + (end.y - ty * length - ny * wing);
|
|
180
|
+
};
|
|
181
|
+
const reviewAnnotations = [...state.review.annotations];
|
|
182
|
+
state.review.workspace?.heldDistances.forEach((distance) => {
|
|
183
|
+
if (!reviewAnnotations.some((annotation) => annotation.type === "measurement" && annotation.kind === "distance" && annotation.id === distance.id)) {
|
|
184
|
+
reviewAnnotations.push({ type: "measurement", kind: "distance", ...distance });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
reviewAnnotations.forEach((annotation) => {
|
|
188
|
+
if (annotation.type === "arrow") {
|
|
189
|
+
const control = annotation.control || midpoint(annotation.start, annotation.end);
|
|
190
|
+
const bezierControl = { x: 2 * control.x - (annotation.start.x + annotation.end.x) / 2, y: 2 * control.y - (annotation.start.y + annotation.end.y) / 2 };
|
|
191
|
+
if (annotation.control) {
|
|
192
|
+
annotations.append(svg("path", { d: "M " + annotation.start.x + " " + annotation.start.y + " Q " + bezierControl.x + " " + bezierControl.y + " " + annotation.end.x + " " + annotation.end.y, fill: "none", stroke: annotation.color, "stroke-width": annotation.width, "stroke-linecap": "round" }));
|
|
193
|
+
} else {
|
|
194
|
+
line(annotation.start.x, annotation.start.y, annotation.end.x, annotation.end.y, annotation.color, annotation.width);
|
|
195
|
+
}
|
|
196
|
+
annotations.append(svg("polyline", { points: arrowHead(annotation.start, control, annotation.end, annotation.width), fill: "none", stroke: annotation.color, "stroke-width": annotation.width, "stroke-linecap": "round", "stroke-linejoin": "round" }));
|
|
197
|
+
} else if (annotation.type === "pen") {
|
|
198
|
+
annotations.append(svg("polyline", { points: annotation.points.map((point) => point.x + "," + point.y).join(" "), fill: "none", stroke: annotation.color, "stroke-width": annotation.width, "stroke-linecap": "round", "stroke-linejoin": "round" }));
|
|
199
|
+
} else if (annotation.type === "text") {
|
|
200
|
+
const textStyle = annotation.style || state.review.textStyle || { font: "sans-serif", color: "#000000" };
|
|
201
|
+
const fontFamily = textStyle.font === "handwritten"
|
|
202
|
+
? '"Segoe Script", "Bradley Hand", "Apple Chancery", cursive'
|
|
203
|
+
: textStyle.font === "code"
|
|
204
|
+
? 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
|
205
|
+
: textStyle.font === "serif"
|
|
206
|
+
? 'ui-serif, Georgia, "Times New Roman", serif'
|
|
207
|
+
: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
|
|
208
|
+
const text = document.createElement("div");
|
|
209
|
+
text.className = "annotation-text";
|
|
210
|
+
text.textContent = annotation.text;
|
|
211
|
+
text.style.left = annotation.x + "px";
|
|
212
|
+
text.style.top = annotation.y + "px";
|
|
213
|
+
text.style.color = textStyle.color;
|
|
214
|
+
text.style.fontFamily = fontFamily;
|
|
215
|
+
text.style.fontSize = (16 * (annotation.scale || 1)) + "px";
|
|
216
|
+
text.style.lineHeight = (24 * (annotation.scale || 1)) + "px";
|
|
217
|
+
if (annotation.boxWidth) text.style.width = annotation.boxWidth + "px";
|
|
218
|
+
if (annotation.rotation) text.style.transform = "rotate(" + annotation.rotation + "deg)";
|
|
219
|
+
textAnnotations.append(text);
|
|
220
|
+
} else if (annotation.type === "guide") {
|
|
221
|
+
if (annotation.orientation === "vertical") line(annotation.position, 0, annotation.position, height, "#0d99ff", 1, "6 4");
|
|
222
|
+
else line(0, annotation.position, width, annotation.position, "#0d99ff", 1, "6 4");
|
|
223
|
+
} else if (annotation.type === "rectangle") {
|
|
224
|
+
annotations.append(svg("rect", { x: annotation.rect.left, y: annotation.rect.top, width: annotation.rect.width, height: annotation.rect.height, fill: "none", stroke: annotation.color, "stroke-width": annotation.width, "stroke-dasharray": "4 3" }));
|
|
225
|
+
} else if (annotation.kind === "element") {
|
|
226
|
+
annotations.append(svg("rect", { x: annotation.rect.left, y: annotation.rect.top, width: annotation.rect.width, height: annotation.rect.height, fill: "none", stroke: "#0d99ff", "stroke-width": 2, "stroke-dasharray": "4 3" }));
|
|
227
|
+
} else {
|
|
228
|
+
annotation.connectors.forEach((connector) => line(connector.x1, connector.y1, connector.x2, connector.y2, "#0d99ff", 1));
|
|
229
|
+
if (annotation.horizontal) line(annotation.horizontal.x1, annotation.horizontal.y, annotation.horizontal.x2, annotation.horizontal.y, "#0d99ff", 2);
|
|
230
|
+
if (annotation.vertical) line(annotation.vertical.x, annotation.vertical.y1, annotation.vertical.x, annotation.vertical.y2, "#0d99ff", 2);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
state.review.comments.forEach((comment, index) => {
|
|
234
|
+
const pin = document.createElement("button");
|
|
235
|
+
pin.className = "comment-pin";
|
|
236
|
+
pin.type = "button";
|
|
237
|
+
pin.textContent = String(index + 1);
|
|
238
|
+
pin.style.left = (comment.position.x / width * 100) + "%";
|
|
239
|
+
pin.style.top = (comment.position.y / height * 100) + "%";
|
|
240
|
+
pin.setAttribute("aria-label", "Comment " + (index + 1));
|
|
241
|
+
const card = document.createElement("aside");
|
|
242
|
+
card.className = "comment-card";
|
|
243
|
+
card.hidden = true;
|
|
244
|
+
card.style.left = (comment.position.x / width * 100) + "%";
|
|
245
|
+
card.style.top = (comment.position.y / height * 100) + "%";
|
|
246
|
+
const heading = document.createElement("strong");
|
|
247
|
+
heading.textContent = comment.status === "resolved" ? "Resolved comment" : "Comment";
|
|
248
|
+
const messages = document.createElement("div");
|
|
249
|
+
const renderMessages = () => {
|
|
250
|
+
messages.replaceChildren();
|
|
251
|
+
const threadMessages = comment.messages.length > 0
|
|
252
|
+
? comment.messages
|
|
253
|
+
: [{ id: "initial", role: "user", text: comment.text, createdAt: comment.createdAt }];
|
|
254
|
+
threadMessages.forEach((message) => {
|
|
255
|
+
const item = document.createElement("div");
|
|
256
|
+
item.className = "comment-message";
|
|
257
|
+
const meta = document.createElement("header");
|
|
258
|
+
meta.textContent = "You \xB7 " + new Date(message.createdAt).toLocaleString();
|
|
259
|
+
const body = document.createElement("p");
|
|
260
|
+
body.textContent = message.text;
|
|
261
|
+
item.append(meta, body);
|
|
262
|
+
messages.append(item);
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
renderMessages();
|
|
266
|
+
const reply = document.createElement("form");
|
|
267
|
+
reply.className = "comment-reply";
|
|
268
|
+
const input = document.createElement("textarea");
|
|
269
|
+
input.rows = 2;
|
|
270
|
+
input.placeholder = "Reply...";
|
|
271
|
+
input.setAttribute("aria-label", "Reply to comment");
|
|
272
|
+
const send = document.createElement("button");
|
|
273
|
+
send.type = "submit";
|
|
274
|
+
send.textContent = "Reply";
|
|
275
|
+
reply.append(input, send);
|
|
276
|
+
reply.addEventListener("submit", (event) => {
|
|
277
|
+
event.preventDefault();
|
|
278
|
+
const text = input.value.trim();
|
|
279
|
+
if (!text) return;
|
|
280
|
+
comment.messages.push({ id: "reply-" + Date.now(), role: "user", text, createdAt: Date.now() });
|
|
281
|
+
comment.text = comment.messages[0]?.text || text;
|
|
282
|
+
input.value = "";
|
|
283
|
+
renderMessages();
|
|
284
|
+
});
|
|
285
|
+
card.append(heading, messages, reply);
|
|
286
|
+
pin.addEventListener("click", () => { card.hidden = !card.hidden; });
|
|
287
|
+
comments.append(pin, card);
|
|
288
|
+
});
|
|
289
|
+
})();
|
|
290
|
+
</script>
|
|
291
|
+
</body>
|
|
292
|
+
</html>`;
|
|
293
|
+
var createEphemeralReviewUrl = async (state, capture, ownerWindow) => {
|
|
294
|
+
const screenshotDataUrl = await blobToDataUrl(capture.blob);
|
|
295
|
+
const nextState = {
|
|
296
|
+
...state,
|
|
297
|
+
versions: state.versions.map(
|
|
298
|
+
(version, index) => index === state.versions.length - 1 ? {
|
|
299
|
+
...version,
|
|
300
|
+
snapshot: {
|
|
301
|
+
...version.snapshot,
|
|
302
|
+
screenshot: {
|
|
303
|
+
id: `screenshot-${version.id}`,
|
|
304
|
+
mimeType: capture.mimeType,
|
|
305
|
+
width: capture.width,
|
|
306
|
+
height: capture.height
|
|
307
|
+
},
|
|
308
|
+
...capture.cssWidth && capture.cssHeight ? { viewport: { ...version.snapshot.viewport, width: capture.cssWidth, height: capture.cssHeight } } : {}
|
|
309
|
+
}
|
|
310
|
+
} : version
|
|
311
|
+
)
|
|
312
|
+
};
|
|
313
|
+
const origin = capture.origin ?? { x: 0, y: 0 };
|
|
314
|
+
const positionedState = {
|
|
315
|
+
...nextState,
|
|
316
|
+
review: {
|
|
317
|
+
...nextState.review,
|
|
318
|
+
comments: nextState.review.comments.map((comment) => ({
|
|
319
|
+
...comment,
|
|
320
|
+
position: {
|
|
321
|
+
x: (comment.position.x - origin.x) * (capture.scale ?? 1),
|
|
322
|
+
y: (comment.position.y - origin.y) * (capture.scale ?? 1)
|
|
323
|
+
}
|
|
324
|
+
})),
|
|
325
|
+
annotations: nextState.review.annotations.map((annotation) => transformAnnotation(annotation, origin, capture.scale ?? 1))
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const payload = encodePayload({
|
|
329
|
+
state: positionedState,
|
|
330
|
+
screenshotDataUrl,
|
|
331
|
+
...capture.live ? { liveSnapshot: capture.live } : {}
|
|
332
|
+
});
|
|
333
|
+
const html = createViewerHtml(payload);
|
|
334
|
+
const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
|
|
335
|
+
return { url, state: positionedState, html };
|
|
336
|
+
};
|
|
337
|
+
var revokeEphemeralReviewUrl = (url, ownerWindow) => {
|
|
338
|
+
void ownerWindow;
|
|
339
|
+
URL.revokeObjectURL(url);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export {
|
|
343
|
+
createEphemeralReviewUrl,
|
|
344
|
+
revokeEphemeralReviewUrl
|
|
345
|
+
};
|