mesurer 0.2.5 → 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 CHANGED
@@ -1,5 +1,19 @@
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
+
9
+ ## 0.2.6
10
+
11
+ - add responsive previews (`B`): the page at several screen sizes side by side, with Inspect, Comments, Guides, and X-ray working in each
12
+ - add comments inside previews, copied grouped by URL and viewport
13
+ - change `M` to toggle Comments, not Mesurer on/off
14
+ - change Persist to on by default
15
+ - fix motion card scope, stuck trimmed video exports, and page shortcuts firing while typing in the extension
16
+
3
17
  ## 0.2.5
4
18
 
5
19
  - add a toolbar dock that snaps to the edges and corners of the screen, with a vertical layout on the left and right; choose Free in Settings to keep the toolbar floating
package/README.md CHANGED
@@ -52,13 +52,13 @@ function App() {
52
52
  | `guideHighlightEnabled` | Highlights guides when they are hovered or selected. Default `true`. |
53
53
  | `hoverHighlightEnabled` | Disables hover highlight and deselects on click when `false`. |
54
54
  | `layoutDetailsEnabled` | Shows gap and padding under selected dimensions. Default `true`. |
55
- | `persistOnReload` | Persists workspace state across reloads when `true`. |
55
+ | `persistOnReload` | Persists workspace state across reloads; defaults to `true`. |
56
56
  | `shortcutsEnabled` | Enables global keyboard shortcuts when `true`. Default `true`. |
57
57
  | `persistKey` | Optional workspace storage key; default workspaces are isolated per browser tab. |
58
58
  | `portalTarget` | Optional element or shadow root used as the overlay portal mount target. |
59
59
  | `persistence` | Optional storage adapter for custom or extension-backed persistence. |
60
60
  | `captureVisibleTab` | Optional function supplying a PNG of the visible tab for Screenshot capture. |
61
- | `features` | Enable or disable `screenshot`, `rulers`, and `settings`; each defaults to `true`. |
61
+ | `features` | Enable or disable `screenshot`, `rulers`, `settings`, and `responsive`; each defaults to `true`. |
62
62
  | `onPersistenceError` | Called when persistence is unavailable or a storage write fails. |
63
63
  | `colorPickerFormats` | Color formats displayed in the picker popover, in display order. |
64
64
  | `colorPickerClickFormat` | Format copied to the clipboard when a color is picked. |
@@ -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` | Default text annotation font and color. |
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
 
@@ -78,7 +78,7 @@ Props are the defaults. Saved settings override them; **Use defaults** restores
78
78
 
79
79
  | Shortcut | Action |
80
80
  | ---------------------- | ----------------------------------------------------- |
81
- | `M` | Toggle Mesurer on/off. |
81
+ | `M` | Toggle Comments mode. |
82
82
  | `I` | Toggle Inspect mode. |
83
83
  | `S` | Toggle Select mode for annotations. |
84
84
  | `A` | Toggle Typography mode. |
@@ -93,6 +93,7 @@ Props are the defaults. Saved settings override them; **Use defaults** restores
93
93
  | `G` | Toggle Guides mode. |
94
94
  | `X` | Toggle X-ray mode. |
95
95
  | `R` | Toggle pixel rulers along the top and left edges. |
96
+ | `B` | Preview the page at other screen sizes. |
96
97
  | `H` | Set guide orientation to horizontal. |
97
98
  | `V` | Set guide orientation to vertical. |
98
99
  | `Alt` | Temporarily enable option/guide measurement overlays. |
@@ -120,13 +121,14 @@ Props are the defaults. Saved settings override them; **Use defaults** restores
120
121
  - **Rulers** - Drag guides from pixel rulers along the top and left edges
121
122
  - **Typography** - Inspect typography styles and pin text details
122
123
  - **X-ray mode** - Reveal element structure without changing the page
124
+ - **Responsive** - Preview the page at several screen sizes side by side, from named devices or its own breakpoints
123
125
  - **Sample color** - Sample rendered colors and copy values in your chosen format
124
126
  - **Screenshot** - Drag a region of the visible tab and copy or download it. In a React integration without the extension, the browser may show a tab-sharing prompt.
125
127
  - **Video recording** - Drag a region of the visible tab, record up to 60 seconds, trim the result, and export it as WebM, GIF, or MP4 when supported. The Chrome extension records the active tab without a second sharing prompt.
126
128
  - **Distance overlays** - Hold Alt for quick spacing checks
127
129
  - **Undo/redo** - Command history for guides, measurements, arrows, pen strokes, and text annotations
128
130
  - **Comments** - Pin threads to live elements, add replies, move targets, search threads, and copy structured feedback for agents
129
- - **Settings** - Configure selection, guide, ruler, arrow, text, color, format, and persistence behavior
131
+ - **Settings** - Configure selection, guide, ruler, arrow, color, format, and persistence behavior
130
132
  - **Workspace controls** - Persist page workspaces, restore defaults, clear annotations, and minimize Mesurer to a single toolbar button
131
133
 
132
134
  ## Requirements
@@ -215,6 +215,7 @@ async function reencodeVideoClip(ownerDocument, ownerWindow, sourceUrl, startTim
215
215
  if (stopped) return;
216
216
  stopped = true;
217
217
  source.removeEventListener("timeupdate", onTimeUpdate);
218
+ source.removeEventListener("ended", finish);
218
219
  source.pause();
219
220
  try {
220
221
  recorder.stop();
@@ -245,18 +246,20 @@ async function reencodeVideoClip(ownerDocument, ownerWindow, sourceUrl, startTim
245
246
  };
246
247
  recorder.onerror = () => reject(new Error("Could not trim recording"));
247
248
  source.addEventListener("timeupdate", onTimeUpdate);
249
+ source.addEventListener("ended", finish);
248
250
  recorder.start(100);
249
251
  void source.play().then(
250
252
  () => drawFrame(),
251
253
  () => {
252
254
  source.removeEventListener("timeupdate", onTimeUpdate);
255
+ source.removeEventListener("ended", finish);
253
256
  reject(new Error("Could not trim recording"));
254
257
  }
255
258
  );
256
259
  });
257
260
  }
258
261
  async function encodeGifClip(ownerDocument, sourceUrl, startTime, endTime, scale) {
259
- const encoder = await import("./screen-recording-gif-WG5J6DEP.js");
262
+ const encoder = await import("./screen-recording-gif-4B4R3ZBM.js");
260
263
  return encoder.encodeGifClip(ownerDocument, sourceUrl, startTime, endTime, scale);
261
264
  }
262
265
 
@@ -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
+ };
@@ -0,0 +1,8 @@
1
+ import {
2
+ createEphemeralReviewUrl,
3
+ revokeEphemeralReviewUrl
4
+ } from "./chunk-R2UAUDQP.js";
5
+ export {
6
+ createEphemeralReviewUrl,
7
+ revokeEphemeralReviewUrl
8
+ };