@getdevteam/analytics-web 0.1.0 → 0.3.0
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/README.md +42 -0
- package/dist/feedback-W5PGVRNZ.js +651 -0
- package/dist/feedback-W5PGVRNZ.js.map +1 -0
- package/dist/index.cjs +751 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -3
- package/dist/index.d.ts +26 -3
- package/dist/index.js +70 -15
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @getdevteam/analytics-web
|
|
2
|
+
|
|
3
|
+
Browser SDK for the [DevTeam Analytics & Logging Platform](https://github.com/getdevteam-ai/analytics).
|
|
4
|
+
Drop it into any browser app - plain JS, Astro, Vue, Svelte, or React without the hooks - to send analytics events and logs.
|
|
5
|
+
Building with React and want a `useAnalytics()` hook? Pair this with [`@getdevteam/analytics-react`](https://www.npmjs.com/package/@getdevteam/analytics-react).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @getdevteam/analytics-web
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createAnalytics } from "@getdevteam/analytics-web";
|
|
17
|
+
|
|
18
|
+
const analytics = createAnalytics({
|
|
19
|
+
key: "dtp_your_public_key",
|
|
20
|
+
host: "https://ingest.analytics.getdevteam.ai",
|
|
21
|
+
trackPageviews: true, // auto-send a pageview on every route change
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
analytics.identify("user_123", { plan: "pro" }); // who the user is
|
|
25
|
+
analytics.track("checkout_completed", { amount: 4200 }); // something that happened
|
|
26
|
+
analytics.log.error("payment gateway timed out", { gateway: "stripe" });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Auto-captures uncaught errors and unhandled promise rejections (turn off with `autoCaptureErrors: false`), and flushes queued events when the page is hidden or closed so nothing is lost on navigation.
|
|
30
|
+
|
|
31
|
+
## User feedback
|
|
32
|
+
|
|
33
|
+
Set `allowUserFeedback: true` to render a floating feedback button (isolated in a Shadow DOM host, loaded lazily): the user gets a screenshot of the page, drawing tools, and a comment box, and the result lands in the platform's feedback inbox.
|
|
34
|
+
`analytics.showFeedback()` opens the overlay programmatically; it is a no-op while the feature is off or unavailable.
|
|
35
|
+
Customize with `feedback: { colors, accentColor, buttonLabel, title, submitLabel, commentPlaceholder, successMessage, position, zIndex, pixelRatio }`.
|
|
36
|
+
|
|
37
|
+
The screenshot is a DOM-to-canvas render: cross-origin iframes, tainted canvases, and video frames come out blank or make the capture fail, and a failed capture falls back to comment-only feedback instead of breaking the app.
|
|
38
|
+
See the [SDK README](https://github.com/getdevteam-ai/analytics/tree/main/src/sdk#user-feedback) for the full option table and capture boundary.
|
|
39
|
+
|
|
40
|
+
You need a public ingest key (`dtp_...`) and the ingest host URL for your environment - see [Getting a key](https://github.com/getdevteam-ai/analytics/tree/main/src/sdk#getting-a-key) in the full SDK docs.
|
|
41
|
+
|
|
42
|
+
Full client API, config options, and release process: [src/sdk README](https://github.com/getdevteam-ai/analytics/tree/main/src/sdk).
|
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
// src/feedback/index.ts
|
|
2
|
+
import { uuidV7 } from "@getdevteam/analytics-core";
|
|
3
|
+
|
|
4
|
+
// src/feedback/capture.ts
|
|
5
|
+
import { toCanvas } from "html-to-image";
|
|
6
|
+
async function captureViewport(exclude, pixelRatio) {
|
|
7
|
+
return toCanvas(document.documentElement, {
|
|
8
|
+
filter: (node) => node !== exclude,
|
|
9
|
+
pixelRatio,
|
|
10
|
+
width: window.innerWidth,
|
|
11
|
+
height: window.innerHeight,
|
|
12
|
+
style: {
|
|
13
|
+
transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)`
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/feedback/exportImage.ts
|
|
19
|
+
var JPEG_QUALITY = 0.85;
|
|
20
|
+
var DOWNSCALE_FACTOR = 0.7;
|
|
21
|
+
var MIN_EXPORT_DIMENSION = 200;
|
|
22
|
+
function drawStrokes(ctx, strokes) {
|
|
23
|
+
for (const stroke of strokes) {
|
|
24
|
+
const [first, ...rest] = stroke.points;
|
|
25
|
+
if (!first) continue;
|
|
26
|
+
ctx.strokeStyle = stroke.color;
|
|
27
|
+
ctx.lineWidth = stroke.width;
|
|
28
|
+
ctx.lineCap = "round";
|
|
29
|
+
ctx.lineJoin = "round";
|
|
30
|
+
ctx.beginPath();
|
|
31
|
+
ctx.moveTo(first.x, first.y);
|
|
32
|
+
if (rest.length === 0) ctx.lineTo(first.x, first.y);
|
|
33
|
+
for (const point of rest) ctx.lineTo(point.x, point.y);
|
|
34
|
+
ctx.stroke();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function exportAnnotatedImage(source, strokes, options) {
|
|
38
|
+
try {
|
|
39
|
+
const composed = compose(source, strokes);
|
|
40
|
+
if (!composed) return null;
|
|
41
|
+
const png = await encode(composed, "image/png");
|
|
42
|
+
if (png && png.size <= options.maxBytes) {
|
|
43
|
+
return { base64: await blobToBase64(png), contentType: "image/png" };
|
|
44
|
+
}
|
|
45
|
+
let canvas = composed;
|
|
46
|
+
let jpeg = await encode(canvas, "image/jpeg", JPEG_QUALITY);
|
|
47
|
+
while (jpeg && jpeg.size > options.maxBytes && Math.min(canvas.width, canvas.height) * DOWNSCALE_FACTOR >= MIN_EXPORT_DIMENSION) {
|
|
48
|
+
const smaller = downscale(canvas, DOWNSCALE_FACTOR);
|
|
49
|
+
if (!smaller) break;
|
|
50
|
+
canvas = smaller;
|
|
51
|
+
jpeg = await encode(canvas, "image/jpeg", JPEG_QUALITY);
|
|
52
|
+
}
|
|
53
|
+
if (jpeg && jpeg.size <= options.maxBytes) {
|
|
54
|
+
return { base64: await blobToBase64(jpeg), contentType: "image/jpeg" };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function compose(source, strokes) {
|
|
62
|
+
const canvas = document.createElement("canvas");
|
|
63
|
+
canvas.width = source.width;
|
|
64
|
+
canvas.height = source.height;
|
|
65
|
+
const ctx = get2dContext(canvas);
|
|
66
|
+
if (!ctx || canvas.width === 0 || canvas.height === 0) return null;
|
|
67
|
+
ctx.fillStyle = "#ffffff";
|
|
68
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
69
|
+
ctx.drawImage(source, 0, 0);
|
|
70
|
+
drawStrokes(ctx, strokes);
|
|
71
|
+
return canvas;
|
|
72
|
+
}
|
|
73
|
+
function downscale(source, factor) {
|
|
74
|
+
const canvas = document.createElement("canvas");
|
|
75
|
+
canvas.width = Math.max(1, Math.round(source.width * factor));
|
|
76
|
+
canvas.height = Math.max(1, Math.round(source.height * factor));
|
|
77
|
+
const ctx = get2dContext(canvas);
|
|
78
|
+
if (!ctx) return null;
|
|
79
|
+
ctx.drawImage(source, 0, 0, canvas.width, canvas.height);
|
|
80
|
+
return canvas;
|
|
81
|
+
}
|
|
82
|
+
function get2dContext(canvas) {
|
|
83
|
+
try {
|
|
84
|
+
return canvas.getContext("2d");
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function encode(canvas, type, quality) {
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
try {
|
|
92
|
+
canvas.toBlob((blob) => resolve(blob), type, quality);
|
|
93
|
+
} catch {
|
|
94
|
+
resolve(null);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function blobToBase64(blob) {
|
|
99
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
100
|
+
let binary = "";
|
|
101
|
+
const chunkSize = 32768;
|
|
102
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
103
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
|
|
104
|
+
}
|
|
105
|
+
return btoa(binary);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/feedback/strokes.ts
|
|
109
|
+
function createStrokeStore() {
|
|
110
|
+
const strokes = [];
|
|
111
|
+
let active;
|
|
112
|
+
return {
|
|
113
|
+
begin(color, width, point) {
|
|
114
|
+
if (active) return;
|
|
115
|
+
active = { points: [point], color, width };
|
|
116
|
+
strokes.push(active);
|
|
117
|
+
},
|
|
118
|
+
extend(point) {
|
|
119
|
+
active?.points.push(point);
|
|
120
|
+
},
|
|
121
|
+
end() {
|
|
122
|
+
active = void 0;
|
|
123
|
+
},
|
|
124
|
+
undo() {
|
|
125
|
+
if (active) return;
|
|
126
|
+
strokes.pop();
|
|
127
|
+
},
|
|
128
|
+
clear() {
|
|
129
|
+
if (active) return;
|
|
130
|
+
strokes.length = 0;
|
|
131
|
+
},
|
|
132
|
+
list: () => strokes,
|
|
133
|
+
isDrawing: () => active !== void 0
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function mapToImagePoint(clientX, clientY, rect, imageWidth, imageHeight) {
|
|
137
|
+
const scaleX = rect.width > 0 && imageWidth > 0 ? imageWidth / rect.width : 1;
|
|
138
|
+
const scaleY = rect.height > 0 && imageHeight > 0 ? imageHeight / rect.height : 1;
|
|
139
|
+
return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/feedback/index.ts
|
|
143
|
+
var DEFAULT_COLORS = ["#f44336", "#4caf50", "#2196f3", "#ffeb3b"];
|
|
144
|
+
var DEFAULT_ACCENT = "#5b9cf5";
|
|
145
|
+
var DEFAULT_Z_INDEX = 2147483e3;
|
|
146
|
+
var PEN_WIDTH = 5;
|
|
147
|
+
var MAX_SCREENSHOT_BYTES = 5 * 1024 * 1024;
|
|
148
|
+
var MAX_TEXT_LENGTH = 1e4;
|
|
149
|
+
var MAX_PAGE_URL_LENGTH = 2048;
|
|
150
|
+
var SUCCESS_CLOSE_DELAY_MS = 1600;
|
|
151
|
+
var defaultDeps = {
|
|
152
|
+
capture: captureViewport,
|
|
153
|
+
exportAnnotated: exportAnnotatedImage
|
|
154
|
+
};
|
|
155
|
+
function mountFeedback(config, deps = defaultDeps) {
|
|
156
|
+
const options = config.options;
|
|
157
|
+
const colors = options.colors && options.colors.length > 0 ? options.colors : DEFAULT_COLORS;
|
|
158
|
+
const accent = options.accentColor ?? DEFAULT_ACCENT;
|
|
159
|
+
const pixelRatio = options.pixelRatio ?? Math.min(typeof devicePixelRatio === "number" && devicePixelRatio > 0 ? devicePixelRatio : 1, 2);
|
|
160
|
+
let destroyed = false;
|
|
161
|
+
let overlayOpen = false;
|
|
162
|
+
let overlaySession = 0;
|
|
163
|
+
let capturing = false;
|
|
164
|
+
let sending = false;
|
|
165
|
+
let draftFeedbackId = uuidV7();
|
|
166
|
+
let sourceCanvas;
|
|
167
|
+
let activeColor = colors[0] ?? DEFAULT_COLORS[0];
|
|
168
|
+
let penWidth = PEN_WIDTH;
|
|
169
|
+
let previousBodyOverflow;
|
|
170
|
+
let closeTimer;
|
|
171
|
+
let drawContext;
|
|
172
|
+
const strokes = createStrokeStore();
|
|
173
|
+
const report = (error2) => {
|
|
174
|
+
try {
|
|
175
|
+
config.onError(error2);
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
const safely = (fn) => {
|
|
180
|
+
try {
|
|
181
|
+
fn();
|
|
182
|
+
} catch (error2) {
|
|
183
|
+
report(error2);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const host = document.createElement("div");
|
|
187
|
+
host.setAttribute("data-devteam-feedback", "");
|
|
188
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
189
|
+
const style = document.createElement("style");
|
|
190
|
+
style.textContent = buildStyles(options, accent);
|
|
191
|
+
const container = element("div", "container");
|
|
192
|
+
const fab = element("button", "fab");
|
|
193
|
+
fab.type = "button";
|
|
194
|
+
fab.textContent = options.buttonLabel ?? "Feedback";
|
|
195
|
+
const overlay = element("div", "overlay");
|
|
196
|
+
overlay.hidden = true;
|
|
197
|
+
const backdrop = element("div", "backdrop");
|
|
198
|
+
const panel = element("div", "panel");
|
|
199
|
+
panel.setAttribute("role", "dialog");
|
|
200
|
+
panel.setAttribute("aria-modal", "true");
|
|
201
|
+
const header = element("div", "header");
|
|
202
|
+
const title = element("div", "title");
|
|
203
|
+
title.textContent = options.title ?? "Send feedback";
|
|
204
|
+
const closeButton = element("button", "close");
|
|
205
|
+
closeButton.type = "button";
|
|
206
|
+
closeButton.setAttribute("aria-label", "Close");
|
|
207
|
+
closeButton.textContent = "\xD7";
|
|
208
|
+
header.append(title, closeButton);
|
|
209
|
+
const preview = element("div", "preview");
|
|
210
|
+
const drawCanvas = document.createElement("canvas");
|
|
211
|
+
drawCanvas.className = "draw";
|
|
212
|
+
preview.appendChild(drawCanvas);
|
|
213
|
+
const tools = element("div", "tools");
|
|
214
|
+
const swatches = colors.map((color) => {
|
|
215
|
+
const swatch = element("button", "swatch");
|
|
216
|
+
swatch.type = "button";
|
|
217
|
+
swatch.style.background = color;
|
|
218
|
+
swatch.setAttribute("aria-label", `Pen color ${color}`);
|
|
219
|
+
swatch.addEventListener("click", () => safely(() => selectColor(color)));
|
|
220
|
+
tools.appendChild(swatch);
|
|
221
|
+
return { color, swatch };
|
|
222
|
+
});
|
|
223
|
+
const undoButton = element("button", "tool undo");
|
|
224
|
+
undoButton.type = "button";
|
|
225
|
+
undoButton.textContent = "Undo";
|
|
226
|
+
const clearButton = element("button", "tool clear");
|
|
227
|
+
clearButton.type = "button";
|
|
228
|
+
clearButton.textContent = "Clear";
|
|
229
|
+
tools.append(undoButton, clearButton);
|
|
230
|
+
const comment = document.createElement("textarea");
|
|
231
|
+
comment.className = "comment";
|
|
232
|
+
comment.placeholder = options.commentPlaceholder ?? "What went wrong, or what could be better?";
|
|
233
|
+
comment.maxLength = MAX_TEXT_LENGTH;
|
|
234
|
+
const error = element("div", "error");
|
|
235
|
+
error.hidden = true;
|
|
236
|
+
const success = element("div", "success");
|
|
237
|
+
success.hidden = true;
|
|
238
|
+
success.textContent = options.successMessage ?? "Thanks for the feedback!";
|
|
239
|
+
const actions = element("div", "actions");
|
|
240
|
+
const cancelButton = element("button", "cancel");
|
|
241
|
+
cancelButton.type = "button";
|
|
242
|
+
cancelButton.textContent = "Cancel";
|
|
243
|
+
const submitButton = element("button", "submit");
|
|
244
|
+
submitButton.type = "button";
|
|
245
|
+
submitButton.textContent = options.submitLabel ?? "Send";
|
|
246
|
+
actions.append(cancelButton, submitButton);
|
|
247
|
+
panel.append(header, preview, tools, comment, error, success, actions);
|
|
248
|
+
overlay.append(backdrop, panel);
|
|
249
|
+
container.append(fab, overlay);
|
|
250
|
+
shadow.append(style, container);
|
|
251
|
+
if (document.body) {
|
|
252
|
+
document.body.appendChild(host);
|
|
253
|
+
} else {
|
|
254
|
+
document.addEventListener(
|
|
255
|
+
"DOMContentLoaded",
|
|
256
|
+
() => {
|
|
257
|
+
if (!destroyed) document.body?.appendChild(host);
|
|
258
|
+
},
|
|
259
|
+
{ once: true }
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
function selectColor(color) {
|
|
263
|
+
activeColor = color;
|
|
264
|
+
for (const entry of swatches) {
|
|
265
|
+
entry.swatch.classList.toggle("active", entry.color === color);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
selectColor(activeColor);
|
|
269
|
+
function context2d() {
|
|
270
|
+
if (drawContext === void 0) {
|
|
271
|
+
try {
|
|
272
|
+
drawContext = drawCanvas.getContext("2d");
|
|
273
|
+
} catch {
|
|
274
|
+
drawContext = null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return drawContext;
|
|
278
|
+
}
|
|
279
|
+
function repaint() {
|
|
280
|
+
const ctx = context2d();
|
|
281
|
+
if (!ctx) return;
|
|
282
|
+
ctx.clearRect(0, 0, drawCanvas.width, drawCanvas.height);
|
|
283
|
+
drawStrokes(ctx, strokes.list());
|
|
284
|
+
}
|
|
285
|
+
function pointFromEvent(event) {
|
|
286
|
+
const { clientX, clientY } = event;
|
|
287
|
+
if (typeof clientX !== "number" || typeof clientY !== "number") return void 0;
|
|
288
|
+
const rect = drawCanvas.getBoundingClientRect();
|
|
289
|
+
return mapToImagePoint(clientX, clientY, rect, drawCanvas.width, drawCanvas.height);
|
|
290
|
+
}
|
|
291
|
+
function withPointerCapture(event, action) {
|
|
292
|
+
const pointerId = event.pointerId;
|
|
293
|
+
if (typeof pointerId !== "number") return;
|
|
294
|
+
const method = action === "set" ? drawCanvas.setPointerCapture : drawCanvas.releasePointerCapture;
|
|
295
|
+
if (typeof method !== "function") return;
|
|
296
|
+
try {
|
|
297
|
+
method.call(drawCanvas, pointerId);
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const onPointerDown = (event) => safely(() => {
|
|
302
|
+
if (!sourceCanvas || sending) return;
|
|
303
|
+
const point = pointFromEvent(event);
|
|
304
|
+
if (!point) return;
|
|
305
|
+
if (typeof event.preventDefault === "function") event.preventDefault();
|
|
306
|
+
withPointerCapture(event, "set");
|
|
307
|
+
strokes.begin(activeColor, penWidth, point);
|
|
308
|
+
repaint();
|
|
309
|
+
});
|
|
310
|
+
const onPointerMove = (event) => safely(() => {
|
|
311
|
+
if (!strokes.isDrawing()) return;
|
|
312
|
+
const point = pointFromEvent(event);
|
|
313
|
+
if (!point) return;
|
|
314
|
+
strokes.extend(point);
|
|
315
|
+
repaint();
|
|
316
|
+
});
|
|
317
|
+
const onPointerEnd = (event) => safely(() => {
|
|
318
|
+
withPointerCapture(event, "release");
|
|
319
|
+
strokes.end();
|
|
320
|
+
});
|
|
321
|
+
drawCanvas.addEventListener("pointerdown", onPointerDown);
|
|
322
|
+
drawCanvas.addEventListener("pointermove", onPointerMove);
|
|
323
|
+
drawCanvas.addEventListener("pointerup", onPointerEnd);
|
|
324
|
+
drawCanvas.addEventListener("pointercancel", onPointerEnd);
|
|
325
|
+
drawCanvas.addEventListener("pointerleave", onPointerEnd);
|
|
326
|
+
shadow.addEventListener("pointerup", onPointerEnd);
|
|
327
|
+
undoButton.addEventListener(
|
|
328
|
+
"click",
|
|
329
|
+
() => safely(() => {
|
|
330
|
+
strokes.undo();
|
|
331
|
+
repaint();
|
|
332
|
+
})
|
|
333
|
+
);
|
|
334
|
+
clearButton.addEventListener(
|
|
335
|
+
"click",
|
|
336
|
+
() => safely(() => {
|
|
337
|
+
strokes.clear();
|
|
338
|
+
repaint();
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
const onKeyDown = (event) => {
|
|
342
|
+
if (event.key === "Escape") safely(close);
|
|
343
|
+
};
|
|
344
|
+
function lockScroll() {
|
|
345
|
+
if (previousBodyOverflow !== void 0) return;
|
|
346
|
+
previousBodyOverflow = document.body.style.overflow;
|
|
347
|
+
document.body.style.overflow = "hidden";
|
|
348
|
+
}
|
|
349
|
+
function unlockScroll() {
|
|
350
|
+
if (previousBodyOverflow === void 0) return;
|
|
351
|
+
document.body.style.overflow = previousBodyOverflow;
|
|
352
|
+
previousBodyOverflow = void 0;
|
|
353
|
+
}
|
|
354
|
+
function setError(message) {
|
|
355
|
+
error.hidden = message === void 0;
|
|
356
|
+
error.textContent = message ?? "";
|
|
357
|
+
}
|
|
358
|
+
function setFormVisible(visible) {
|
|
359
|
+
const hasPreview = sourceCanvas !== void 0;
|
|
360
|
+
preview.hidden = !visible || !hasPreview;
|
|
361
|
+
tools.hidden = !visible || !hasPreview;
|
|
362
|
+
comment.hidden = !visible;
|
|
363
|
+
actions.hidden = !visible;
|
|
364
|
+
}
|
|
365
|
+
function showOverlay(canvas) {
|
|
366
|
+
overlaySession += 1;
|
|
367
|
+
draftFeedbackId = uuidV7();
|
|
368
|
+
sourceCanvas = canvas;
|
|
369
|
+
strokes.end();
|
|
370
|
+
strokes.clear();
|
|
371
|
+
comment.value = "";
|
|
372
|
+
setError(void 0);
|
|
373
|
+
success.hidden = true;
|
|
374
|
+
submitButton.disabled = false;
|
|
375
|
+
if (canvas) {
|
|
376
|
+
canvas.classList.add("shot");
|
|
377
|
+
preview.insertBefore(canvas, drawCanvas);
|
|
378
|
+
drawCanvas.width = canvas.width;
|
|
379
|
+
drawCanvas.height = canvas.height;
|
|
380
|
+
penWidth = PEN_WIDTH * (pixelRatio > 0 ? pixelRatio : 1);
|
|
381
|
+
repaint();
|
|
382
|
+
}
|
|
383
|
+
selectColor(colors[0] ?? DEFAULT_COLORS[0]);
|
|
384
|
+
setFormVisible(true);
|
|
385
|
+
overlay.hidden = false;
|
|
386
|
+
overlayOpen = true;
|
|
387
|
+
lockScroll();
|
|
388
|
+
document.addEventListener("keydown", onKeyDown);
|
|
389
|
+
}
|
|
390
|
+
function close() {
|
|
391
|
+
if (!overlayOpen) return;
|
|
392
|
+
overlaySession += 1;
|
|
393
|
+
overlayOpen = false;
|
|
394
|
+
overlay.hidden = true;
|
|
395
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
396
|
+
unlockScroll();
|
|
397
|
+
if (closeTimer !== void 0) {
|
|
398
|
+
clearTimeout(closeTimer);
|
|
399
|
+
closeTimer = void 0;
|
|
400
|
+
}
|
|
401
|
+
sending = false;
|
|
402
|
+
if (sourceCanvas) {
|
|
403
|
+
sourceCanvas.remove();
|
|
404
|
+
sourceCanvas = void 0;
|
|
405
|
+
}
|
|
406
|
+
strokes.end();
|
|
407
|
+
strokes.clear();
|
|
408
|
+
}
|
|
409
|
+
function open() {
|
|
410
|
+
if (destroyed || overlayOpen || capturing) return;
|
|
411
|
+
capturing = true;
|
|
412
|
+
void Promise.resolve().then(() => deps.capture(host, pixelRatio)).then(
|
|
413
|
+
(canvas) => {
|
|
414
|
+
capturing = false;
|
|
415
|
+
if (destroyed) return;
|
|
416
|
+
showOverlay(canvas);
|
|
417
|
+
},
|
|
418
|
+
(captureError) => {
|
|
419
|
+
capturing = false;
|
|
420
|
+
if (destroyed) return;
|
|
421
|
+
report(captureError);
|
|
422
|
+
showOverlay(void 0);
|
|
423
|
+
}
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
function buildBody(text, screenshot) {
|
|
427
|
+
const body = {
|
|
428
|
+
feedback_id: draftFeedbackId,
|
|
429
|
+
text: text.slice(0, MAX_TEXT_LENGTH)
|
|
430
|
+
};
|
|
431
|
+
if (screenshot) {
|
|
432
|
+
body.screenshot = screenshot.base64;
|
|
433
|
+
body.screenshot_content_type = screenshot.contentType;
|
|
434
|
+
}
|
|
435
|
+
const distinctId = config.getDistinctId();
|
|
436
|
+
if (distinctId) body.distinct_id = distinctId;
|
|
437
|
+
const sessionId = config.getSessionId();
|
|
438
|
+
if (sessionId) body.session_id = sessionId;
|
|
439
|
+
if (typeof location !== "undefined" && location.href) {
|
|
440
|
+
body.page_url = location.href.slice(0, MAX_PAGE_URL_LENGTH);
|
|
441
|
+
}
|
|
442
|
+
body.context = config.getContext();
|
|
443
|
+
return JSON.stringify(body);
|
|
444
|
+
}
|
|
445
|
+
async function submit() {
|
|
446
|
+
if (sending || !overlayOpen) return;
|
|
447
|
+
const text = comment.value.trim();
|
|
448
|
+
if (text.length === 0) {
|
|
449
|
+
setError("Please add a short comment before sending.");
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const session = overlaySession;
|
|
453
|
+
const sameSession = () => !destroyed && overlayOpen && overlaySession === session;
|
|
454
|
+
sending = true;
|
|
455
|
+
submitButton.disabled = true;
|
|
456
|
+
setError(void 0);
|
|
457
|
+
try {
|
|
458
|
+
let screenshot = null;
|
|
459
|
+
if (sourceCanvas) {
|
|
460
|
+
screenshot = await deps.exportAnnotated(sourceCanvas, strokes.list(), {
|
|
461
|
+
maxBytes: MAX_SCREENSHOT_BYTES
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
if (!config.fetch) throw new Error("fetch is not available in this environment");
|
|
465
|
+
const response = await config.fetch(config.url, {
|
|
466
|
+
method: "POST",
|
|
467
|
+
headers: { "Content-Type": "application/json", "X-DevTeam-Key": config.key },
|
|
468
|
+
body: buildBody(text, screenshot)
|
|
469
|
+
});
|
|
470
|
+
if (!sameSession()) return;
|
|
471
|
+
if (response.ok) {
|
|
472
|
+
setFormVisible(false);
|
|
473
|
+
success.hidden = false;
|
|
474
|
+
closeTimer = setTimeout(() => safely(close), SUCCESS_CLOSE_DELAY_MS);
|
|
475
|
+
} else {
|
|
476
|
+
setError("Could not send feedback. Please try again.");
|
|
477
|
+
}
|
|
478
|
+
} catch (submitError) {
|
|
479
|
+
report(submitError);
|
|
480
|
+
if (sameSession()) setError("Could not send feedback. Please try again.");
|
|
481
|
+
} finally {
|
|
482
|
+
if (overlaySession === session) {
|
|
483
|
+
sending = false;
|
|
484
|
+
submitButton.disabled = false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
fab.addEventListener("click", () => safely(open));
|
|
489
|
+
closeButton.addEventListener("click", () => safely(close));
|
|
490
|
+
cancelButton.addEventListener("click", () => safely(close));
|
|
491
|
+
backdrop.addEventListener("click", () => safely(close));
|
|
492
|
+
submitButton.addEventListener("click", () => safely(() => void submit()));
|
|
493
|
+
return {
|
|
494
|
+
open: () => safely(open),
|
|
495
|
+
destroy: () => {
|
|
496
|
+
if (destroyed) return;
|
|
497
|
+
destroyed = true;
|
|
498
|
+
safely(() => {
|
|
499
|
+
close();
|
|
500
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
501
|
+
host.remove();
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function element(tag, className) {
|
|
507
|
+
const node = document.createElement(tag);
|
|
508
|
+
node.className = className;
|
|
509
|
+
return node;
|
|
510
|
+
}
|
|
511
|
+
function buildStyles(options, accent) {
|
|
512
|
+
const zIndex = options.zIndex ?? DEFAULT_Z_INDEX;
|
|
513
|
+
const side = options.position === "bottom-left" ? "left" : "right";
|
|
514
|
+
return `
|
|
515
|
+
:host { all: initial; }
|
|
516
|
+
* { box-sizing: border-box; }
|
|
517
|
+
[hidden] { display: none !important; }
|
|
518
|
+
.container {
|
|
519
|
+
position: fixed;
|
|
520
|
+
inset: 0;
|
|
521
|
+
z-index: ${zIndex};
|
|
522
|
+
pointer-events: none;
|
|
523
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
524
|
+
color: #1f2933;
|
|
525
|
+
}
|
|
526
|
+
.fab {
|
|
527
|
+
position: absolute;
|
|
528
|
+
bottom: 20px;
|
|
529
|
+
${side}: 20px;
|
|
530
|
+
pointer-events: auto;
|
|
531
|
+
border: none;
|
|
532
|
+
border-radius: 999px;
|
|
533
|
+
padding: 10px 18px;
|
|
534
|
+
background: ${accent};
|
|
535
|
+
color: #fff;
|
|
536
|
+
font-size: 14px;
|
|
537
|
+
font-weight: 600;
|
|
538
|
+
cursor: pointer;
|
|
539
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
|
540
|
+
}
|
|
541
|
+
.overlay {
|
|
542
|
+
position: absolute;
|
|
543
|
+
inset: 0;
|
|
544
|
+
pointer-events: auto;
|
|
545
|
+
display: flex;
|
|
546
|
+
align-items: center;
|
|
547
|
+
justify-content: center;
|
|
548
|
+
}
|
|
549
|
+
.backdrop {
|
|
550
|
+
position: absolute;
|
|
551
|
+
inset: 0;
|
|
552
|
+
background: rgba(15, 23, 42, 0.55);
|
|
553
|
+
}
|
|
554
|
+
.panel {
|
|
555
|
+
position: relative;
|
|
556
|
+
display: flex;
|
|
557
|
+
flex-direction: column;
|
|
558
|
+
gap: 10px;
|
|
559
|
+
width: min(560px, calc(100vw - 32px));
|
|
560
|
+
max-height: calc(100vh - 32px);
|
|
561
|
+
overflow: auto;
|
|
562
|
+
background: #fff;
|
|
563
|
+
border-radius: 12px;
|
|
564
|
+
padding: 16px;
|
|
565
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
|
|
566
|
+
}
|
|
567
|
+
.header { display: flex; align-items: center; justify-content: space-between; }
|
|
568
|
+
.title { font-size: 16px; font-weight: 600; }
|
|
569
|
+
.close {
|
|
570
|
+
border: none;
|
|
571
|
+
background: none;
|
|
572
|
+
font-size: 20px;
|
|
573
|
+
line-height: 1;
|
|
574
|
+
cursor: pointer;
|
|
575
|
+
color: #52606d;
|
|
576
|
+
padding: 4px 8px;
|
|
577
|
+
}
|
|
578
|
+
.preview {
|
|
579
|
+
position: relative;
|
|
580
|
+
border: 1px solid #d3dce6;
|
|
581
|
+
border-radius: 8px;
|
|
582
|
+
overflow: hidden;
|
|
583
|
+
background: #f5f7fa;
|
|
584
|
+
}
|
|
585
|
+
.preview .shot { display: block; width: 100%; height: auto; }
|
|
586
|
+
.preview .draw {
|
|
587
|
+
position: absolute;
|
|
588
|
+
inset: 0;
|
|
589
|
+
width: 100%;
|
|
590
|
+
height: 100%;
|
|
591
|
+
cursor: crosshair;
|
|
592
|
+
touch-action: none;
|
|
593
|
+
}
|
|
594
|
+
.tools { display: flex; align-items: center; gap: 8px; }
|
|
595
|
+
.swatch {
|
|
596
|
+
width: 24px;
|
|
597
|
+
height: 24px;
|
|
598
|
+
border-radius: 50%;
|
|
599
|
+
border: 2px solid transparent;
|
|
600
|
+
cursor: pointer;
|
|
601
|
+
padding: 0;
|
|
602
|
+
}
|
|
603
|
+
.swatch.active { border-color: #1f2933; }
|
|
604
|
+
.tool {
|
|
605
|
+
border: 1px solid #d3dce6;
|
|
606
|
+
background: #fff;
|
|
607
|
+
border-radius: 6px;
|
|
608
|
+
padding: 4px 10px;
|
|
609
|
+
font-size: 13px;
|
|
610
|
+
cursor: pointer;
|
|
611
|
+
}
|
|
612
|
+
.comment {
|
|
613
|
+
width: 100%;
|
|
614
|
+
min-height: 72px;
|
|
615
|
+
resize: vertical;
|
|
616
|
+
border: 1px solid #d3dce6;
|
|
617
|
+
border-radius: 8px;
|
|
618
|
+
padding: 8px 10px;
|
|
619
|
+
font: inherit;
|
|
620
|
+
}
|
|
621
|
+
.error { color: #b91c1c; font-size: 13px; }
|
|
622
|
+
.success { color: #15803d; font-size: 14px; font-weight: 600; }
|
|
623
|
+
.actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
624
|
+
.cancel {
|
|
625
|
+
border: 1px solid #d3dce6;
|
|
626
|
+
background: #fff;
|
|
627
|
+
border-radius: 8px;
|
|
628
|
+
padding: 8px 14px;
|
|
629
|
+
font-size: 14px;
|
|
630
|
+
cursor: pointer;
|
|
631
|
+
}
|
|
632
|
+
.submit {
|
|
633
|
+
border: none;
|
|
634
|
+
background: ${accent};
|
|
635
|
+
color: #fff;
|
|
636
|
+
border-radius: 8px;
|
|
637
|
+
padding: 8px 16px;
|
|
638
|
+
font-size: 14px;
|
|
639
|
+
font-weight: 600;
|
|
640
|
+
cursor: pointer;
|
|
641
|
+
}
|
|
642
|
+
.submit:disabled { opacity: 0.6; cursor: default; }
|
|
643
|
+
`;
|
|
644
|
+
}
|
|
645
|
+
export {
|
|
646
|
+
createStrokeStore,
|
|
647
|
+
exportAnnotatedImage,
|
|
648
|
+
mapToImagePoint,
|
|
649
|
+
mountFeedback
|
|
650
|
+
};
|
|
651
|
+
//# sourceMappingURL=feedback-W5PGVRNZ.js.map
|