@checkflow/sdk 1.1.2 → 1.1.4

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/index.mjs CHANGED
@@ -1,11 +1,7 @@
1
1
  import {
2
2
  captureScreenshot,
3
- clearScreenshot,
4
- loadHtml2Canvas
5
- } from "./chunk-CQ56DMFR.mjs";
6
- import {
7
- startHighlighting
8
- } from "./chunk-CD33QAA6.mjs";
3
+ clearScreenshot
4
+ } from "./chunk-6EVTRC5X.mjs";
9
5
 
10
6
  // src/collector.ts
11
7
  function collectContext() {
@@ -111,6 +107,1157 @@ function clearLogs() {
111
107
  errorsCapture = [];
112
108
  }
113
109
 
110
+ // src/network-interceptor.ts
111
+ var MAX_LOGS2 = 50;
112
+ var logs2 = [];
113
+ var installed2 = false;
114
+ function installNetworkInterceptor() {
115
+ if (installed2 || typeof window === "undefined") return;
116
+ installed2 = true;
117
+ const origFetch = window.fetch;
118
+ window.fetch = async function(...args) {
119
+ const start = Date.now();
120
+ const req = new Request(...args);
121
+ const entry = {
122
+ method: req.method,
123
+ url: req.url,
124
+ type: "fetch",
125
+ timestamp: start
126
+ };
127
+ try {
128
+ const res = await origFetch.apply(this, args);
129
+ entry.status = res.status;
130
+ entry.duration = Date.now() - start;
131
+ entry.response_type = res.headers.get("content-type") || void 0;
132
+ pushLog(entry);
133
+ return res;
134
+ } catch (err) {
135
+ entry.duration = Date.now() - start;
136
+ entry.error = err.message || "Network error";
137
+ pushLog(entry);
138
+ throw err;
139
+ }
140
+ };
141
+ const origOpen = XMLHttpRequest.prototype.open;
142
+ const origSend = XMLHttpRequest.prototype.send;
143
+ XMLHttpRequest.prototype.open = function(method, url, ...rest) {
144
+ this.__cf_method = method;
145
+ this.__cf_url = String(url);
146
+ return origOpen.apply(this, [method, url, ...rest]);
147
+ };
148
+ XMLHttpRequest.prototype.send = function(...args) {
149
+ const start = Date.now();
150
+ const xhr = this;
151
+ xhr.addEventListener("loadend", () => {
152
+ const entry = {
153
+ method: xhr.__cf_method || "GET",
154
+ url: xhr.__cf_url || "",
155
+ status: xhr.status,
156
+ duration: Date.now() - start,
157
+ type: "xhr",
158
+ timestamp: start,
159
+ response_type: xhr.getResponseHeader("content-type") || void 0
160
+ };
161
+ if (xhr.status === 0) entry.error = "Request failed";
162
+ pushLog(entry);
163
+ });
164
+ return origSend.apply(this, args);
165
+ };
166
+ }
167
+ function pushLog(entry) {
168
+ if (entry.url.includes("/sdk/feedback")) return;
169
+ logs2.push(entry);
170
+ if (logs2.length > MAX_LOGS2) logs2.shift();
171
+ }
172
+ function getNetworkLogs() {
173
+ return [...logs2];
174
+ }
175
+ function clearNetworkLogs() {
176
+ logs2 = [];
177
+ }
178
+
179
+ // src/annotation/types.ts
180
+ var DEFAULT_STYLE = {
181
+ strokeColor: "#FF3B30",
182
+ strokeWidth: 3,
183
+ fillColor: "transparent",
184
+ opacity: 1,
185
+ fontSize: 16,
186
+ fontFamily: "system-ui, -apple-system, sans-serif"
187
+ };
188
+ var HIGHLIGHT_STYLE = {
189
+ strokeColor: "transparent",
190
+ fillColor: "#FFEB3B",
191
+ opacity: 0.4
192
+ };
193
+ var BLUR_STYLE = {
194
+ strokeColor: "#666666",
195
+ fillColor: "#666666",
196
+ opacity: 0.8
197
+ };
198
+ var COLOR_PALETTE = [
199
+ "#FF3B30",
200
+ // Red
201
+ "#FF9500",
202
+ // Orange
203
+ "#FFCC00",
204
+ // Yellow
205
+ "#34C759",
206
+ // Green
207
+ "#007AFF",
208
+ // Blue
209
+ "#5856D6",
210
+ // Purple
211
+ "#AF52DE",
212
+ // Pink
213
+ "#000000",
214
+ // Black
215
+ "#FFFFFF"
216
+ // White
217
+ ];
218
+ var STROKE_WIDTHS = [1, 2, 3, 5, 8];
219
+
220
+ // src/annotation/toolbar.ts
221
+ var TOOL_ICONS = {
222
+ select: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/><path d="M13 13l6 6"/></svg>`,
223
+ rectangle: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>`,
224
+ ellipse: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="12" rx="9" ry="6"/></svg>`,
225
+ arrow: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>`,
226
+ line: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="19" x2="19" y2="5"/></svg>`,
227
+ highlight: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>`,
228
+ blur: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 13c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm0 4c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm0-8c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm-3 .5c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM6 5c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm15 5.5c.28 0 .5-.22.5-.5s-.22-.5-.5-.5-.5.22-.5.5.22.5.5.5zM14 7c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm0-3.5c.28 0 .5-.22.5-.5s-.22-.5-.5-.5-.5.22-.5.5.22.5.5.5zm-11 10c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm7 7c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm0-17c.28 0 .5-.22.5-.5s-.22-.5-.5-.5-.5.22-.5.5.22.5.5.5zM10 7c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm0 5.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm8 .5c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm0 4c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm0-8c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm0-4c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm3 8.5c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM14 17c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm0 3.5c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm-4-12c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0 8.5c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm4-4.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-4c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z"/></svg>`,
229
+ text: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
230
+ freehand: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>`
231
+ };
232
+ var TOOL_LABELS = {
233
+ select: "S\xE9lection",
234
+ rectangle: "Rectangle",
235
+ ellipse: "Ellipse",
236
+ arrow: "Fl\xE8che",
237
+ line: "Ligne",
238
+ highlight: "Surbrillance",
239
+ blur: "Floutage",
240
+ text: "Texte",
241
+ freehand: "Dessin libre"
242
+ };
243
+ var AnnotationToolbar = class {
244
+ constructor(config2) {
245
+ this.colorPicker = null;
246
+ this.strokePicker = null;
247
+ this.config = config2;
248
+ this.element = this.createToolbar();
249
+ }
250
+ getElement() {
251
+ return this.element;
252
+ }
253
+ setActiveTool(tool) {
254
+ this.config.activeTool = tool;
255
+ this.updateActiveState();
256
+ }
257
+ setStyle(style) {
258
+ this.config.style = style;
259
+ this.updateStyleDisplay();
260
+ }
261
+ createToolbar() {
262
+ const toolbar = document.createElement("div");
263
+ toolbar.className = "cf-toolbar";
264
+ const toolsSection = document.createElement("div");
265
+ toolsSection.className = "cf-toolbar-section cf-toolbar-tools";
266
+ for (const tool of this.config.tools) {
267
+ const button = this.createToolButton(tool);
268
+ toolsSection.appendChild(button);
269
+ }
270
+ const separator1 = document.createElement("div");
271
+ separator1.className = "cf-toolbar-separator";
272
+ const styleSection = document.createElement("div");
273
+ styleSection.className = "cf-toolbar-section cf-toolbar-style";
274
+ const colorBtn = document.createElement("button");
275
+ colorBtn.className = "cf-toolbar-btn cf-color-btn";
276
+ colorBtn.title = "Couleur";
277
+ colorBtn.innerHTML = `<span class="cf-color-preview" style="background: ${this.config.style.strokeColor}"></span>`;
278
+ colorBtn.addEventListener("click", () => this.toggleColorPicker());
279
+ styleSection.appendChild(colorBtn);
280
+ const strokeBtn = document.createElement("button");
281
+ strokeBtn.className = "cf-toolbar-btn cf-stroke-btn";
282
+ strokeBtn.title = "\xC9paisseur";
283
+ strokeBtn.innerHTML = `<span class="cf-stroke-preview">${this.config.style.strokeWidth}px</span>`;
284
+ strokeBtn.addEventListener("click", () => this.toggleStrokePicker());
285
+ styleSection.appendChild(strokeBtn);
286
+ const separator2 = document.createElement("div");
287
+ separator2.className = "cf-toolbar-separator";
288
+ const actionsSection = document.createElement("div");
289
+ actionsSection.className = "cf-toolbar-section cf-toolbar-actions";
290
+ const undoBtn = document.createElement("button");
291
+ undoBtn.className = "cf-toolbar-btn";
292
+ undoBtn.title = "Annuler (Ctrl+Z)";
293
+ undoBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"/></svg>`;
294
+ undoBtn.addEventListener("click", () => this.config.onUndo());
295
+ actionsSection.appendChild(undoBtn);
296
+ const clearBtn = document.createElement("button");
297
+ clearBtn.className = "cf-toolbar-btn";
298
+ clearBtn.title = "Tout effacer";
299
+ clearBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>`;
300
+ clearBtn.addEventListener("click", () => this.config.onClear());
301
+ actionsSection.appendChild(clearBtn);
302
+ toolbar.appendChild(toolsSection);
303
+ toolbar.appendChild(separator1);
304
+ toolbar.appendChild(styleSection);
305
+ toolbar.appendChild(separator2);
306
+ toolbar.appendChild(actionsSection);
307
+ return toolbar;
308
+ }
309
+ createToolButton(tool) {
310
+ const button = document.createElement("button");
311
+ button.className = `cf-toolbar-btn cf-tool-btn ${tool === this.config.activeTool ? "active" : ""}`;
312
+ button.dataset.tool = tool;
313
+ button.title = TOOL_LABELS[tool];
314
+ button.innerHTML = TOOL_ICONS[tool];
315
+ button.addEventListener("click", () => {
316
+ this.config.onToolChange(tool);
317
+ this.setActiveTool(tool);
318
+ });
319
+ return button;
320
+ }
321
+ updateActiveState() {
322
+ const buttons = this.element.querySelectorAll(".cf-tool-btn");
323
+ buttons.forEach((btn) => {
324
+ const button = btn;
325
+ button.classList.toggle("active", button.dataset.tool === this.config.activeTool);
326
+ });
327
+ }
328
+ updateStyleDisplay() {
329
+ const colorPreview = this.element.querySelector(".cf-color-preview");
330
+ if (colorPreview) {
331
+ colorPreview.style.background = this.config.style.strokeColor;
332
+ }
333
+ const strokePreview = this.element.querySelector(".cf-stroke-preview");
334
+ if (strokePreview) {
335
+ strokePreview.textContent = `${this.config.style.strokeWidth}px`;
336
+ }
337
+ }
338
+ toggleColorPicker() {
339
+ if (this.colorPicker) {
340
+ this.colorPicker.remove();
341
+ this.colorPicker = null;
342
+ return;
343
+ }
344
+ if (this.strokePicker) {
345
+ this.strokePicker.remove();
346
+ this.strokePicker = null;
347
+ }
348
+ this.colorPicker = document.createElement("div");
349
+ this.colorPicker.className = "cf-picker cf-color-picker";
350
+ for (const color of COLOR_PALETTE) {
351
+ const swatch = document.createElement("button");
352
+ swatch.className = `cf-color-swatch ${color === this.config.style.strokeColor ? "active" : ""}`;
353
+ swatch.style.background = color;
354
+ swatch.addEventListener("click", () => {
355
+ this.config.onStyleChange({ strokeColor: color });
356
+ this.setStyle({ ...this.config.style, strokeColor: color });
357
+ this.colorPicker?.remove();
358
+ this.colorPicker = null;
359
+ });
360
+ this.colorPicker.appendChild(swatch);
361
+ }
362
+ const colorBtn = this.element.querySelector(".cf-color-btn");
363
+ colorBtn?.appendChild(this.colorPicker);
364
+ }
365
+ toggleStrokePicker() {
366
+ if (this.strokePicker) {
367
+ this.strokePicker.remove();
368
+ this.strokePicker = null;
369
+ return;
370
+ }
371
+ if (this.colorPicker) {
372
+ this.colorPicker.remove();
373
+ this.colorPicker = null;
374
+ }
375
+ this.strokePicker = document.createElement("div");
376
+ this.strokePicker.className = "cf-picker cf-stroke-picker";
377
+ for (const width of STROKE_WIDTHS) {
378
+ const option = document.createElement("button");
379
+ option.className = `cf-stroke-option ${width === this.config.style.strokeWidth ? "active" : ""}`;
380
+ option.innerHTML = `<span style="height: ${width}px"></span> ${width}px`;
381
+ option.addEventListener("click", () => {
382
+ this.config.onStyleChange({ strokeWidth: width });
383
+ this.setStyle({ ...this.config.style, strokeWidth: width });
384
+ this.strokePicker?.remove();
385
+ this.strokePicker = null;
386
+ });
387
+ this.strokePicker.appendChild(option);
388
+ }
389
+ const strokeBtn = this.element.querySelector(".cf-stroke-btn");
390
+ strokeBtn?.appendChild(this.strokePicker);
391
+ }
392
+ };
393
+
394
+ // src/annotation/styles.ts
395
+ var STYLES = `
396
+ /* Annotation Overlay */
397
+ .cf-annotation-overlay {
398
+ position: fixed;
399
+ top: 0;
400
+ left: 0;
401
+ right: 0;
402
+ bottom: 0;
403
+ z-index: 999999;
404
+ background: rgba(0, 0, 0, 0.85);
405
+ display: flex;
406
+ align-items: center;
407
+ justify-content: center;
408
+ animation: cf-fade-in 0.2s ease-out;
409
+ }
410
+
411
+ @keyframes cf-fade-in {
412
+ from { opacity: 0; }
413
+ to { opacity: 1; }
414
+ }
415
+
416
+ /* Editor Wrapper */
417
+ .cf-annotation-wrapper {
418
+ display: flex;
419
+ flex-direction: column;
420
+ max-width: 95vw;
421
+ max-height: 95vh;
422
+ background: #1a1a1a;
423
+ border-radius: 12px;
424
+ overflow: hidden;
425
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
426
+ }
427
+
428
+ /* Canvas Container */
429
+ .cf-annotation-canvas-container {
430
+ flex: 1;
431
+ display: flex;
432
+ align-items: center;
433
+ justify-content: center;
434
+ overflow: auto;
435
+ padding: 16px;
436
+ background: #0d0d0d;
437
+ }
438
+
439
+ .cf-annotation-canvas {
440
+ max-width: 100%;
441
+ max-height: calc(95vh - 140px);
442
+ border-radius: 4px;
443
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
444
+ cursor: crosshair;
445
+ }
446
+
447
+ /* Toolbar */
448
+ .cf-toolbar {
449
+ display: flex;
450
+ align-items: center;
451
+ gap: 8px;
452
+ padding: 12px 16px;
453
+ background: #2a2a2a;
454
+ border-bottom: 1px solid #3a3a3a;
455
+ }
456
+
457
+ .cf-toolbar-section {
458
+ display: flex;
459
+ align-items: center;
460
+ gap: 4px;
461
+ }
462
+
463
+ .cf-toolbar-separator {
464
+ width: 1px;
465
+ height: 24px;
466
+ background: #4a4a4a;
467
+ margin: 0 8px;
468
+ }
469
+
470
+ .cf-toolbar-btn {
471
+ display: flex;
472
+ align-items: center;
473
+ justify-content: center;
474
+ width: 36px;
475
+ height: 36px;
476
+ padding: 0;
477
+ border: none;
478
+ border-radius: 8px;
479
+ background: transparent;
480
+ color: #999;
481
+ cursor: pointer;
482
+ transition: all 0.15s ease;
483
+ }
484
+
485
+ .cf-toolbar-btn:hover {
486
+ background: #3a3a3a;
487
+ color: #fff;
488
+ }
489
+
490
+ .cf-toolbar-btn.active {
491
+ background: #007AFF;
492
+ color: #fff;
493
+ }
494
+
495
+ .cf-toolbar-btn svg {
496
+ width: 20px;
497
+ height: 20px;
498
+ }
499
+
500
+ /* Color Button */
501
+ .cf-color-btn {
502
+ position: relative;
503
+ }
504
+
505
+ .cf-color-preview {
506
+ width: 20px;
507
+ height: 20px;
508
+ border-radius: 50%;
509
+ border: 2px solid #fff;
510
+ box-shadow: 0 0 0 1px rgba(0,0,0,0.2);
511
+ }
512
+
513
+ /* Stroke Button */
514
+ .cf-stroke-btn {
515
+ width: auto;
516
+ padding: 0 12px;
517
+ }
518
+
519
+ .cf-stroke-preview {
520
+ font-size: 12px;
521
+ font-weight: 500;
522
+ color: inherit;
523
+ }
524
+
525
+ /* Pickers */
526
+ .cf-picker {
527
+ position: absolute;
528
+ top: 100%;
529
+ left: 50%;
530
+ transform: translateX(-50%);
531
+ margin-top: 8px;
532
+ padding: 8px;
533
+ background: #2a2a2a;
534
+ border-radius: 8px;
535
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
536
+ z-index: 10;
537
+ }
538
+
539
+ .cf-color-picker {
540
+ display: grid;
541
+ grid-template-columns: repeat(5, 1fr);
542
+ gap: 4px;
543
+ width: 140px;
544
+ }
545
+
546
+ .cf-color-swatch {
547
+ width: 24px;
548
+ height: 24px;
549
+ border-radius: 50%;
550
+ border: 2px solid transparent;
551
+ cursor: pointer;
552
+ transition: transform 0.15s ease;
553
+ }
554
+
555
+ .cf-color-swatch:hover {
556
+ transform: scale(1.15);
557
+ }
558
+
559
+ .cf-color-swatch.active {
560
+ border-color: #fff;
561
+ }
562
+
563
+ .cf-stroke-picker {
564
+ display: flex;
565
+ flex-direction: column;
566
+ gap: 4px;
567
+ min-width: 80px;
568
+ }
569
+
570
+ .cf-stroke-option {
571
+ display: flex;
572
+ align-items: center;
573
+ gap: 8px;
574
+ padding: 6px 10px;
575
+ border: none;
576
+ border-radius: 4px;
577
+ background: transparent;
578
+ color: #999;
579
+ font-size: 12px;
580
+ cursor: pointer;
581
+ transition: all 0.15s ease;
582
+ }
583
+
584
+ .cf-stroke-option:hover {
585
+ background: #3a3a3a;
586
+ color: #fff;
587
+ }
588
+
589
+ .cf-stroke-option.active {
590
+ background: #007AFF;
591
+ color: #fff;
592
+ }
593
+
594
+ .cf-stroke-option span {
595
+ width: 24px;
596
+ background: currentColor;
597
+ border-radius: 2px;
598
+ }
599
+
600
+ /* Action Buttons */
601
+ .cf-annotation-actions {
602
+ display: flex;
603
+ justify-content: flex-end;
604
+ gap: 12px;
605
+ padding: 12px 16px;
606
+ background: #2a2a2a;
607
+ border-top: 1px solid #3a3a3a;
608
+ }
609
+
610
+ .cf-btn {
611
+ display: flex;
612
+ align-items: center;
613
+ justify-content: center;
614
+ padding: 10px 20px;
615
+ border: none;
616
+ border-radius: 8px;
617
+ font-size: 14px;
618
+ font-weight: 500;
619
+ cursor: pointer;
620
+ transition: all 0.15s ease;
621
+ }
622
+
623
+ .cf-btn-primary {
624
+ background: #007AFF;
625
+ color: #fff;
626
+ }
627
+
628
+ .cf-btn-primary:hover {
629
+ background: #0066DD;
630
+ }
631
+
632
+ .cf-btn-secondary {
633
+ background: #3a3a3a;
634
+ color: #fff;
635
+ }
636
+
637
+ .cf-btn-secondary:hover {
638
+ background: #4a4a4a;
639
+ }
640
+
641
+ /* Text Input */
642
+ .cf-annotation-text-input {
643
+ position: fixed;
644
+ z-index: 1000000;
645
+ padding: 4px 8px;
646
+ border: 2px solid #007AFF;
647
+ border-radius: 4px;
648
+ background: rgba(255, 255, 255, 0.95);
649
+ font-size: 16px;
650
+ font-family: system-ui, -apple-system, sans-serif;
651
+ outline: none;
652
+ min-width: 150px;
653
+ }
654
+
655
+ /* Tool-specific cursors */
656
+ .cf-annotation-canvas[data-tool="select"] { cursor: default; }
657
+ .cf-annotation-canvas[data-tool="rectangle"] { cursor: crosshair; }
658
+ .cf-annotation-canvas[data-tool="ellipse"] { cursor: crosshair; }
659
+ .cf-annotation-canvas[data-tool="arrow"] { cursor: crosshair; }
660
+ .cf-annotation-canvas[data-tool="line"] { cursor: crosshair; }
661
+ .cf-annotation-canvas[data-tool="highlight"] { cursor: crosshair; }
662
+ .cf-annotation-canvas[data-tool="blur"] { cursor: crosshair; }
663
+ .cf-annotation-canvas[data-tool="text"] { cursor: text; }
664
+ .cf-annotation-canvas[data-tool="freehand"] { cursor: crosshair; }
665
+
666
+ /* Responsive */
667
+ @media (max-width: 768px) {
668
+ .cf-toolbar {
669
+ flex-wrap: wrap;
670
+ justify-content: center;
671
+ }
672
+
673
+ .cf-toolbar-separator {
674
+ display: none;
675
+ }
676
+
677
+ .cf-annotation-actions {
678
+ justify-content: stretch;
679
+ }
680
+
681
+ .cf-annotation-actions .cf-btn {
682
+ flex: 1;
683
+ }
684
+ }
685
+ `;
686
+ var stylesInjected = false;
687
+ function injectAnnotationStyles() {
688
+ if (stylesInjected) return;
689
+ const styleElement = document.createElement("style");
690
+ styleElement.id = "cf-annotation-styles";
691
+ styleElement.textContent = STYLES;
692
+ document.head.appendChild(styleElement);
693
+ stylesInjected = true;
694
+ }
695
+
696
+ // src/annotation/editor.ts
697
+ var AnnotationEditor = class {
698
+ constructor(config2) {
699
+ this.container = null;
700
+ this.canvas = null;
701
+ this.ctx = null;
702
+ this.toolbar = null;
703
+ this.annotations = [];
704
+ this.backgroundImage = null;
705
+ this.state = {
706
+ activeTool: "rectangle",
707
+ style: { ...DEFAULT_STYLE },
708
+ isDrawing: false,
709
+ currentAnnotation: null
710
+ };
711
+ this.startPoint = null;
712
+ this.freehandPoints = [];
713
+ this.textInput = null;
714
+ this.config = {
715
+ ...config2,
716
+ tools: config2.tools || ["select", "rectangle", "arrow", "highlight", "blur", "text", "freehand"],
717
+ defaultStyle: config2.defaultStyle || DEFAULT_STYLE
718
+ };
719
+ this.state.style = { ...this.config.defaultStyle };
720
+ }
721
+ /**
722
+ * Open the annotation editor with a screenshot
723
+ */
724
+ async open(screenshotDataUrl2) {
725
+ injectAnnotationStyles();
726
+ this.backgroundImage = await this.loadImage(screenshotDataUrl2);
727
+ this.createEditorUI();
728
+ this.setupEventListeners();
729
+ this.render();
730
+ }
731
+ /**
732
+ * Close the editor
733
+ */
734
+ close() {
735
+ if (this.container) {
736
+ this.container.remove();
737
+ this.container = null;
738
+ }
739
+ this.canvas = null;
740
+ this.ctx = null;
741
+ this.toolbar = null;
742
+ this.annotations = [];
743
+ this.backgroundImage = null;
744
+ }
745
+ /**
746
+ * Get the annotated image as data URL
747
+ */
748
+ getAnnotatedImage() {
749
+ if (!this.canvas || !this.ctx) return "";
750
+ const tempCanvas = document.createElement("canvas");
751
+ tempCanvas.width = this.canvas.width;
752
+ tempCanvas.height = this.canvas.height;
753
+ const tempCtx = tempCanvas.getContext("2d");
754
+ if (this.backgroundImage) {
755
+ tempCtx.drawImage(this.backgroundImage, 0, 0);
756
+ }
757
+ this.drawAnnotations(tempCtx);
758
+ return tempCanvas.toDataURL("image/png");
759
+ }
760
+ /**
761
+ * Get annotations data
762
+ */
763
+ getAnnotations() {
764
+ return [...this.annotations];
765
+ }
766
+ // Private methods
767
+ loadImage(src) {
768
+ return new Promise((resolve, reject) => {
769
+ const img = new Image();
770
+ img.onload = () => resolve(img);
771
+ img.onerror = reject;
772
+ img.src = src;
773
+ });
774
+ }
775
+ createEditorUI() {
776
+ this.container = document.createElement("div");
777
+ this.container.className = "cf-annotation-overlay";
778
+ const wrapper = document.createElement("div");
779
+ wrapper.className = "cf-annotation-wrapper";
780
+ const canvasContainer = document.createElement("div");
781
+ canvasContainer.className = "cf-annotation-canvas-container";
782
+ this.canvas = document.createElement("canvas");
783
+ this.canvas.className = "cf-annotation-canvas";
784
+ this.canvas.width = this.backgroundImage?.width || 1920;
785
+ this.canvas.height = this.backgroundImage?.height || 1080;
786
+ this.ctx = this.canvas.getContext("2d");
787
+ canvasContainer.appendChild(this.canvas);
788
+ this.toolbar = new AnnotationToolbar({
789
+ tools: this.config.tools,
790
+ activeTool: this.state.activeTool,
791
+ style: this.state.style,
792
+ onToolChange: (tool) => this.setActiveTool(tool),
793
+ onStyleChange: (style) => this.setStyle(style),
794
+ onUndo: () => this.undo(),
795
+ onClear: () => this.clearAll(),
796
+ onSave: () => this.save(),
797
+ onCancel: () => this.cancel()
798
+ });
799
+ const actions = document.createElement("div");
800
+ actions.className = "cf-annotation-actions";
801
+ actions.innerHTML = `
802
+ <button class="cf-btn cf-btn-secondary" data-action="cancel">Annuler</button>
803
+ <button class="cf-btn cf-btn-primary" data-action="save">Enregistrer</button>
804
+ `;
805
+ wrapper.appendChild(this.toolbar.getElement());
806
+ wrapper.appendChild(canvasContainer);
807
+ wrapper.appendChild(actions);
808
+ this.container.appendChild(wrapper);
809
+ document.body.appendChild(this.container);
810
+ actions.querySelector('[data-action="cancel"]')?.addEventListener("click", () => this.cancel());
811
+ actions.querySelector('[data-action="save"]')?.addEventListener("click", () => this.save());
812
+ }
813
+ setupEventListeners() {
814
+ if (!this.canvas) return;
815
+ this.canvas.addEventListener("mousedown", this.handleMouseDown.bind(this));
816
+ this.canvas.addEventListener("mousemove", this.handleMouseMove.bind(this));
817
+ this.canvas.addEventListener("mouseup", this.handleMouseUp.bind(this));
818
+ this.canvas.addEventListener("mouseleave", this.handleMouseUp.bind(this));
819
+ this.canvas.addEventListener("touchstart", this.handleTouchStart.bind(this));
820
+ this.canvas.addEventListener("touchmove", this.handleTouchMove.bind(this));
821
+ this.canvas.addEventListener("touchend", this.handleTouchEnd.bind(this));
822
+ document.addEventListener("keydown", this.handleKeyDown.bind(this));
823
+ }
824
+ handleMouseDown(e) {
825
+ const point = this.getCanvasPoint(e);
826
+ this.startDrawing(point);
827
+ }
828
+ handleMouseMove(e) {
829
+ const point = this.getCanvasPoint(e);
830
+ this.continueDrawing(point);
831
+ }
832
+ handleMouseUp(_e) {
833
+ this.finishDrawing();
834
+ }
835
+ handleTouchStart(e) {
836
+ e.preventDefault();
837
+ const touch = e.touches[0];
838
+ const point = this.getCanvasPointFromTouch(touch);
839
+ this.startDrawing(point);
840
+ }
841
+ handleTouchMove(e) {
842
+ e.preventDefault();
843
+ const touch = e.touches[0];
844
+ const point = this.getCanvasPointFromTouch(touch);
845
+ this.continueDrawing(point);
846
+ }
847
+ handleTouchEnd(e) {
848
+ e.preventDefault();
849
+ this.finishDrawing();
850
+ }
851
+ handleKeyDown(e) {
852
+ if ((e.ctrlKey || e.metaKey) && e.key === "z") {
853
+ e.preventDefault();
854
+ this.undo();
855
+ }
856
+ if (e.key === "Escape") {
857
+ if (this.state.isDrawing) {
858
+ this.state.isDrawing = false;
859
+ this.state.currentAnnotation = null;
860
+ this.render();
861
+ } else {
862
+ this.cancel();
863
+ }
864
+ }
865
+ if (e.key === "Enter" && !this.state.isDrawing) {
866
+ this.save();
867
+ }
868
+ }
869
+ getCanvasPoint(e) {
870
+ const rect = this.canvas.getBoundingClientRect();
871
+ const scaleX = this.canvas.width / rect.width;
872
+ const scaleY = this.canvas.height / rect.height;
873
+ return {
874
+ x: (e.clientX - rect.left) * scaleX,
875
+ y: (e.clientY - rect.top) * scaleY
876
+ };
877
+ }
878
+ getCanvasPointFromTouch(touch) {
879
+ const rect = this.canvas.getBoundingClientRect();
880
+ const scaleX = this.canvas.width / rect.width;
881
+ const scaleY = this.canvas.height / rect.height;
882
+ return {
883
+ x: (touch.clientX - rect.left) * scaleX,
884
+ y: (touch.clientY - rect.top) * scaleY
885
+ };
886
+ }
887
+ startDrawing(point) {
888
+ if (this.state.activeTool === "select") return;
889
+ this.state.isDrawing = true;
890
+ this.startPoint = point;
891
+ if (this.state.activeTool === "text") {
892
+ this.showTextInput(point);
893
+ return;
894
+ }
895
+ if (this.state.activeTool === "freehand") {
896
+ this.freehandPoints = [point];
897
+ }
898
+ }
899
+ continueDrawing(point) {
900
+ if (!this.state.isDrawing || !this.startPoint) return;
901
+ if (this.state.activeTool === "freehand") {
902
+ this.freehandPoints.push(point);
903
+ }
904
+ this.state.currentAnnotation = this.createAnnotation(this.startPoint, point);
905
+ this.render();
906
+ }
907
+ finishDrawing() {
908
+ if (!this.state.isDrawing || !this.state.currentAnnotation) {
909
+ this.state.isDrawing = false;
910
+ return;
911
+ }
912
+ if (this.isValidAnnotation(this.state.currentAnnotation)) {
913
+ this.annotations.push(this.state.currentAnnotation);
914
+ }
915
+ this.state.isDrawing = false;
916
+ this.state.currentAnnotation = null;
917
+ this.startPoint = null;
918
+ this.freehandPoints = [];
919
+ this.render();
920
+ }
921
+ isValidAnnotation(annotation) {
922
+ switch (annotation.type) {
923
+ case "rectangle":
924
+ case "ellipse":
925
+ case "highlight":
926
+ case "blur":
927
+ const bounds = annotation.bounds;
928
+ return Math.abs(bounds.width) > 5 && Math.abs(bounds.height) > 5;
929
+ case "arrow":
930
+ case "line":
931
+ const start = annotation.start;
932
+ const end = annotation.end;
933
+ const dist = Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2));
934
+ return dist > 10;
935
+ case "freehand":
936
+ return annotation.points.length > 2;
937
+ case "text":
938
+ return !!annotation.text?.trim();
939
+ default:
940
+ return true;
941
+ }
942
+ }
943
+ createAnnotation(start, end) {
944
+ const id = this.generateId();
945
+ const baseStyle = { ...this.state.style };
946
+ switch (this.state.activeTool) {
947
+ case "rectangle":
948
+ return {
949
+ id,
950
+ type: "rectangle",
951
+ bounds: this.createBounds(start, end),
952
+ style: baseStyle,
953
+ timestamp: Date.now()
954
+ };
955
+ case "ellipse":
956
+ return {
957
+ id,
958
+ type: "ellipse",
959
+ bounds: this.createBounds(start, end),
960
+ style: baseStyle,
961
+ timestamp: Date.now()
962
+ };
963
+ case "arrow":
964
+ return {
965
+ id,
966
+ type: "arrow",
967
+ start: { ...start },
968
+ end: { ...end },
969
+ style: baseStyle,
970
+ timestamp: Date.now()
971
+ };
972
+ case "line":
973
+ return {
974
+ id,
975
+ type: "line",
976
+ start: { ...start },
977
+ end: { ...end },
978
+ style: baseStyle,
979
+ timestamp: Date.now()
980
+ };
981
+ case "highlight":
982
+ return {
983
+ id,
984
+ type: "highlight",
985
+ bounds: this.createBounds(start, end),
986
+ style: { ...baseStyle, ...HIGHLIGHT_STYLE },
987
+ timestamp: Date.now()
988
+ };
989
+ case "blur":
990
+ return {
991
+ id,
992
+ type: "blur",
993
+ bounds: this.createBounds(start, end),
994
+ style: { ...baseStyle, ...BLUR_STYLE },
995
+ blurAmount: 10,
996
+ timestamp: Date.now()
997
+ };
998
+ case "freehand":
999
+ return {
1000
+ id,
1001
+ type: "freehand",
1002
+ points: [...this.freehandPoints],
1003
+ style: baseStyle,
1004
+ timestamp: Date.now()
1005
+ };
1006
+ default:
1007
+ return {
1008
+ id,
1009
+ type: "rectangle",
1010
+ bounds: this.createBounds(start, end),
1011
+ style: baseStyle,
1012
+ timestamp: Date.now()
1013
+ };
1014
+ }
1015
+ }
1016
+ createBounds(start, end) {
1017
+ return {
1018
+ x: Math.min(start.x, end.x),
1019
+ y: Math.min(start.y, end.y),
1020
+ width: Math.abs(end.x - start.x),
1021
+ height: Math.abs(end.y - start.y)
1022
+ };
1023
+ }
1024
+ showTextInput(point) {
1025
+ if (this.textInput) {
1026
+ this.textInput.remove();
1027
+ }
1028
+ const rect = this.canvas.getBoundingClientRect();
1029
+ const scaleX = rect.width / this.canvas.width;
1030
+ const scaleY = rect.height / this.canvas.height;
1031
+ this.textInput = document.createElement("input");
1032
+ this.textInput.type = "text";
1033
+ this.textInput.className = "cf-annotation-text-input";
1034
+ this.textInput.style.left = `${rect.left + point.x * scaleX}px`;
1035
+ this.textInput.style.top = `${rect.top + point.y * scaleY}px`;
1036
+ this.textInput.style.color = this.state.style.strokeColor;
1037
+ this.textInput.style.fontSize = `${(this.state.style.fontSize || 16) * scaleY}px`;
1038
+ this.textInput.placeholder = "Tapez votre texte...";
1039
+ document.body.appendChild(this.textInput);
1040
+ this.textInput.focus();
1041
+ const handleTextSubmit = () => {
1042
+ if (this.textInput && this.textInput.value.trim()) {
1043
+ const textAnnotation = {
1044
+ id: this.generateId(),
1045
+ type: "text",
1046
+ position: point,
1047
+ text: this.textInput.value.trim(),
1048
+ style: { ...this.state.style },
1049
+ timestamp: Date.now()
1050
+ };
1051
+ this.annotations.push(textAnnotation);
1052
+ this.render();
1053
+ }
1054
+ this.textInput?.remove();
1055
+ this.textInput = null;
1056
+ this.state.isDrawing = false;
1057
+ };
1058
+ this.textInput.addEventListener("blur", handleTextSubmit);
1059
+ this.textInput.addEventListener("keydown", (e) => {
1060
+ if (e.key === "Enter") {
1061
+ handleTextSubmit();
1062
+ } else if (e.key === "Escape") {
1063
+ this.textInput?.remove();
1064
+ this.textInput = null;
1065
+ this.state.isDrawing = false;
1066
+ }
1067
+ });
1068
+ }
1069
+ generateId() {
1070
+ return `ann_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1071
+ }
1072
+ setActiveTool(tool) {
1073
+ this.state.activeTool = tool;
1074
+ this.toolbar?.setActiveTool(tool);
1075
+ if (this.canvas) {
1076
+ this.canvas.style.cursor = tool === "select" ? "default" : "crosshair";
1077
+ }
1078
+ }
1079
+ setStyle(style) {
1080
+ this.state.style = { ...this.state.style, ...style };
1081
+ this.toolbar?.setStyle(this.state.style);
1082
+ }
1083
+ undo() {
1084
+ if (this.annotations.length > 0) {
1085
+ this.annotations.pop();
1086
+ this.render();
1087
+ }
1088
+ }
1089
+ clearAll() {
1090
+ this.annotations = [];
1091
+ this.render();
1092
+ }
1093
+ save() {
1094
+ const imageData = this.getAnnotatedImage();
1095
+ this.config.onSave?.(this.annotations, imageData);
1096
+ this.close();
1097
+ }
1098
+ cancel() {
1099
+ this.config.onCancel?.();
1100
+ this.close();
1101
+ }
1102
+ render() {
1103
+ if (!this.ctx || !this.canvas) return;
1104
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1105
+ if (this.backgroundImage) {
1106
+ this.ctx.drawImage(this.backgroundImage, 0, 0);
1107
+ }
1108
+ this.drawAnnotations(this.ctx);
1109
+ if (this.state.currentAnnotation) {
1110
+ this.drawAnnotation(this.ctx, this.state.currentAnnotation);
1111
+ }
1112
+ }
1113
+ drawAnnotations(ctx) {
1114
+ for (const annotation of this.annotations) {
1115
+ this.drawAnnotation(ctx, annotation);
1116
+ }
1117
+ }
1118
+ drawAnnotation(ctx, annotation) {
1119
+ ctx.save();
1120
+ ctx.globalAlpha = annotation.style.opacity;
1121
+ ctx.strokeStyle = annotation.style.strokeColor;
1122
+ ctx.fillStyle = annotation.style.fillColor;
1123
+ ctx.lineWidth = annotation.style.strokeWidth;
1124
+ ctx.lineCap = "round";
1125
+ ctx.lineJoin = "round";
1126
+ switch (annotation.type) {
1127
+ case "rectangle":
1128
+ this.drawRectangle(ctx, annotation.bounds, annotation.style);
1129
+ break;
1130
+ case "ellipse":
1131
+ this.drawEllipse(ctx, annotation.bounds, annotation.style);
1132
+ break;
1133
+ case "arrow":
1134
+ this.drawArrow(ctx, annotation.start, annotation.end, annotation.style);
1135
+ break;
1136
+ case "line":
1137
+ this.drawLine(ctx, annotation.start, annotation.end);
1138
+ break;
1139
+ case "highlight":
1140
+ this.drawHighlight(ctx, annotation.bounds, annotation.style);
1141
+ break;
1142
+ case "blur":
1143
+ this.drawBlur(ctx, annotation.bounds, annotation.blurAmount);
1144
+ break;
1145
+ case "text":
1146
+ this.drawText(ctx, annotation.position, annotation.text, annotation.style);
1147
+ break;
1148
+ case "freehand":
1149
+ this.drawFreehand(ctx, annotation.points);
1150
+ break;
1151
+ }
1152
+ ctx.restore();
1153
+ }
1154
+ drawRectangle(ctx, bounds, style) {
1155
+ if (style.fillColor !== "transparent") {
1156
+ ctx.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
1157
+ }
1158
+ ctx.strokeRect(bounds.x, bounds.y, bounds.width, bounds.height);
1159
+ }
1160
+ drawEllipse(ctx, bounds, style) {
1161
+ const centerX = bounds.x + bounds.width / 2;
1162
+ const centerY = bounds.y + bounds.height / 2;
1163
+ const radiusX = bounds.width / 2;
1164
+ const radiusY = bounds.height / 2;
1165
+ ctx.beginPath();
1166
+ ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
1167
+ if (style.fillColor !== "transparent") {
1168
+ ctx.fill();
1169
+ }
1170
+ ctx.stroke();
1171
+ }
1172
+ drawArrow(ctx, start, end, style) {
1173
+ const headLength = 15 + style.strokeWidth * 2;
1174
+ const angle = Math.atan2(end.y - start.y, end.x - start.x);
1175
+ ctx.beginPath();
1176
+ ctx.moveTo(start.x, start.y);
1177
+ ctx.lineTo(end.x, end.y);
1178
+ ctx.stroke();
1179
+ ctx.beginPath();
1180
+ ctx.moveTo(end.x, end.y);
1181
+ ctx.lineTo(
1182
+ end.x - headLength * Math.cos(angle - Math.PI / 6),
1183
+ end.y - headLength * Math.sin(angle - Math.PI / 6)
1184
+ );
1185
+ ctx.lineTo(
1186
+ end.x - headLength * Math.cos(angle + Math.PI / 6),
1187
+ end.y - headLength * Math.sin(angle + Math.PI / 6)
1188
+ );
1189
+ ctx.closePath();
1190
+ ctx.fillStyle = style.strokeColor;
1191
+ ctx.fill();
1192
+ }
1193
+ drawLine(ctx, start, end) {
1194
+ ctx.beginPath();
1195
+ ctx.moveTo(start.x, start.y);
1196
+ ctx.lineTo(end.x, end.y);
1197
+ ctx.stroke();
1198
+ }
1199
+ drawHighlight(ctx, bounds, style) {
1200
+ ctx.fillStyle = style.fillColor;
1201
+ ctx.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
1202
+ }
1203
+ drawBlur(ctx, bounds, blurAmount) {
1204
+ const pixelSize = Math.max(blurAmount, 5);
1205
+ if (this.backgroundImage) {
1206
+ const tempCanvas = document.createElement("canvas");
1207
+ tempCanvas.width = bounds.width;
1208
+ tempCanvas.height = bounds.height;
1209
+ const tempCtx = tempCanvas.getContext("2d");
1210
+ tempCtx.drawImage(
1211
+ this.backgroundImage,
1212
+ bounds.x,
1213
+ bounds.y,
1214
+ bounds.width,
1215
+ bounds.height,
1216
+ 0,
1217
+ 0,
1218
+ bounds.width,
1219
+ bounds.height
1220
+ );
1221
+ const w = tempCanvas.width;
1222
+ const h = tempCanvas.height;
1223
+ tempCtx.imageSmoothingEnabled = false;
1224
+ tempCtx.drawImage(tempCanvas, 0, 0, w, h, 0, 0, w / pixelSize, h / pixelSize);
1225
+ tempCtx.drawImage(tempCanvas, 0, 0, w / pixelSize, h / pixelSize, 0, 0, w, h);
1226
+ ctx.drawImage(tempCanvas, bounds.x, bounds.y);
1227
+ }
1228
+ ctx.strokeStyle = "#999";
1229
+ ctx.lineWidth = 1;
1230
+ ctx.setLineDash([5, 5]);
1231
+ ctx.strokeRect(bounds.x, bounds.y, bounds.width, bounds.height);
1232
+ ctx.setLineDash([]);
1233
+ }
1234
+ drawText(ctx, position, text, style) {
1235
+ ctx.font = `${style.fontSize || 16}px ${style.fontFamily || "system-ui"}`;
1236
+ ctx.fillStyle = style.strokeColor;
1237
+ ctx.textBaseline = "top";
1238
+ const metrics = ctx.measureText(text);
1239
+ const padding = 4;
1240
+ ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
1241
+ ctx.fillRect(
1242
+ position.x - padding,
1243
+ position.y - padding,
1244
+ metrics.width + padding * 2,
1245
+ (style.fontSize || 16) + padding * 2
1246
+ );
1247
+ ctx.fillStyle = style.strokeColor;
1248
+ ctx.fillText(text, position.x, position.y);
1249
+ }
1250
+ drawFreehand(ctx, points) {
1251
+ if (points.length < 2) return;
1252
+ ctx.beginPath();
1253
+ ctx.moveTo(points[0].x, points[0].y);
1254
+ for (let i = 1; i < points.length; i++) {
1255
+ ctx.lineTo(points[i].x, points[i].y);
1256
+ }
1257
+ ctx.stroke();
1258
+ }
1259
+ };
1260
+
114
1261
  // src/widget.ts
115
1262
  var DEFAULT_CONFIG = {
116
1263
  position: "bottom-right",
@@ -121,79 +1268,168 @@ var DEFAULT_CONFIG = {
121
1268
  var container = null;
122
1269
  var onSubmitCallback = null;
123
1270
  var screenshotDataUrl = null;
1271
+ var annotatedImageUrl = null;
124
1272
  var annotationsData = [];
1273
+ var isExpanded = false;
1274
+ var currentCfg = { ...DEFAULT_CONFIG };
125
1275
  function mountWidget(config2 = {}, onSubmit, opts) {
126
1276
  if (container) return;
127
1277
  onSubmitCallback = onSubmit;
128
1278
  const cfg = { ...DEFAULT_CONFIG, ...config2 };
1279
+ currentCfg = cfg;
129
1280
  container = document.createElement("div");
130
1281
  container.id = "checkflow-widget";
131
- container.innerHTML = getWidgetHTML(cfg);
1282
+ injectStyles(cfg);
1283
+ container.innerHTML = getTriggerHTML(cfg);
132
1284
  document.body.appendChild(container);
133
1285
  const btn = container.querySelector("#cf-trigger");
134
- const form = container.querySelector("#cf-form");
135
- const closeBtn = container.querySelector("#cf-close");
136
- const submitBtn = container.querySelector("#cf-submit");
137
- const screenshotBtn = container.querySelector("#cf-screenshot");
138
- const highlightBtn = container.querySelector("#cf-highlight");
139
- const screenshotPreview = container.querySelector("#cf-screenshot-preview");
140
- btn?.addEventListener("click", () => {
141
- form.style.display = form.style.display === "none" ? "flex" : "none";
142
- screenshotDataUrl = null;
143
- annotationsData = [];
144
- if (screenshotPreview) screenshotPreview.style.display = "none";
1286
+ btn?.addEventListener("click", () => openModal(cfg, opts));
1287
+ }
1288
+ function getBackdropClass(cfg, expanded) {
1289
+ if (expanded) return "cf-backdrop--center";
1290
+ if (cfg.position === "center") return "cf-backdrop--center";
1291
+ return "cf-backdrop--corner cf-pos-" + cfg.position;
1292
+ }
1293
+ function openModal(cfg, opts) {
1294
+ if (!container) return;
1295
+ const existing = document.getElementById("cf-modal-backdrop");
1296
+ if (existing) existing.remove();
1297
+ isExpanded = false;
1298
+ screenshotDataUrl = null;
1299
+ annotatedImageUrl = null;
1300
+ annotationsData = [];
1301
+ const backdrop = document.createElement("div");
1302
+ backdrop.id = "cf-modal-backdrop";
1303
+ backdrop.className = getBackdropClass(cfg, false);
1304
+ backdrop.innerHTML = getModalHTML(cfg, false);
1305
+ document.body.appendChild(backdrop);
1306
+ backdrop.addEventListener("click", (e) => {
1307
+ if (e.target === backdrop) closeModal();
145
1308
  });
146
- closeBtn?.addEventListener("click", () => {
147
- form.style.display = "none";
1309
+ bindModalEvents(cfg, opts);
1310
+ }
1311
+ function closeModal() {
1312
+ const backdrop = document.getElementById("cf-modal-backdrop");
1313
+ if (backdrop) backdrop.remove();
1314
+ isExpanded = false;
1315
+ screenshotDataUrl = null;
1316
+ annotatedImageUrl = null;
1317
+ annotationsData = [];
1318
+ }
1319
+ function expandModal(cfg, opts) {
1320
+ const backdrop = document.getElementById("cf-modal-backdrop");
1321
+ if (!backdrop) return;
1322
+ const name = backdrop.querySelector("#cf-name")?.value || "";
1323
+ const email = backdrop.querySelector("#cf-email")?.value || "";
1324
+ const desc = backdrop.querySelector("#cf-desc")?.value || "";
1325
+ isExpanded = true;
1326
+ backdrop.className = getBackdropClass(cfg, true);
1327
+ backdrop.innerHTML = getModalHTML(cfg, true);
1328
+ bindModalEvents(cfg, opts);
1329
+ const nameEl = backdrop.querySelector("#cf-name");
1330
+ const emailEl = backdrop.querySelector("#cf-email");
1331
+ const descEl = backdrop.querySelector("#cf-desc");
1332
+ if (nameEl) nameEl.value = name;
1333
+ if (emailEl) emailEl.value = email;
1334
+ if (descEl) descEl.value = desc;
1335
+ }
1336
+ function collapseModal(cfg, opts) {
1337
+ const backdrop = document.getElementById("cf-modal-backdrop");
1338
+ if (!backdrop) return;
1339
+ const name = backdrop.querySelector("#cf-name")?.value || "";
1340
+ const email = backdrop.querySelector("#cf-email")?.value || "";
1341
+ const desc = backdrop.querySelector("#cf-desc")?.value || "";
1342
+ isExpanded = false;
1343
+ screenshotDataUrl = null;
1344
+ annotatedImageUrl = null;
1345
+ annotationsData = [];
1346
+ backdrop.className = getBackdropClass(cfg, false);
1347
+ backdrop.innerHTML = getModalHTML(cfg, false);
1348
+ bindModalEvents(cfg, opts);
1349
+ const nameEl = backdrop.querySelector("#cf-name");
1350
+ const emailEl = backdrop.querySelector("#cf-email");
1351
+ const descEl = backdrop.querySelector("#cf-desc");
1352
+ if (nameEl) nameEl.value = name;
1353
+ if (emailEl) emailEl.value = email;
1354
+ if (descEl) descEl.value = desc;
1355
+ }
1356
+ function openAnnotationEditor(cfg, opts) {
1357
+ if (!screenshotDataUrl) return;
1358
+ const backdrop = document.getElementById("cf-modal-backdrop");
1359
+ if (backdrop) backdrop.style.display = "none";
1360
+ const editor = new AnnotationEditor({
1361
+ tools: ["rectangle", "ellipse", "arrow", "highlight", "blur", "text", "freehand"],
1362
+ defaultStyle: { strokeColor: "#FF3B30", strokeWidth: 3, fillColor: "transparent", opacity: 1, fontSize: 16, fontFamily: "system-ui, -apple-system, sans-serif" },
1363
+ onSave: (annotations, imageData) => {
1364
+ annotationsData = annotations;
1365
+ annotatedImageUrl = imageData;
1366
+ if (backdrop) {
1367
+ backdrop.style.display = "flex";
1368
+ const img = backdrop.querySelector("#cf-screenshot-img");
1369
+ if (img) img.src = annotatedImageUrl;
1370
+ const annotBtn = backdrop.querySelector("#cf-annotate-btn");
1371
+ if (annotBtn) annotBtn.textContent = `Annoter (${annotationsData.length})`;
1372
+ }
1373
+ },
1374
+ onCancel: () => {
1375
+ if (backdrop) backdrop.style.display = "flex";
1376
+ }
148
1377
  });
149
- screenshotBtn?.addEventListener("click", async () => {
1378
+ editor.open(screenshotDataUrl);
1379
+ }
1380
+ function bindModalEvents(cfg, opts) {
1381
+ const backdrop = document.getElementById("cf-modal-backdrop");
1382
+ if (!backdrop) return;
1383
+ backdrop.querySelector("#cf-cancel")?.addEventListener("click", closeModal);
1384
+ backdrop.querySelector("#cf-screenshot-btn")?.addEventListener("click", async () => {
150
1385
  if (!opts?.onScreenshot) return;
151
- screenshotBtn.textContent = "Capturing...";
152
- form.style.display = "none";
1386
+ backdrop.style.display = "none";
153
1387
  try {
154
1388
  const data = await opts.onScreenshot();
155
1389
  if (data) {
156
1390
  screenshotDataUrl = data;
157
- if (screenshotPreview) {
158
- screenshotPreview.querySelector("img").src = data;
159
- screenshotPreview.style.display = "block";
160
- }
1391
+ backdrop.style.display = "flex";
1392
+ expandModal(cfg, opts);
1393
+ return;
161
1394
  }
162
1395
  } catch {
163
1396
  }
164
- form.style.display = "flex";
165
- screenshotBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg> Screenshot';
1397
+ backdrop.style.display = "flex";
166
1398
  });
167
- highlightBtn?.addEventListener("click", async () => {
168
- if (!opts?.onHighlight) return;
169
- form.style.display = "none";
170
- try {
171
- const annotations = await opts.onHighlight();
172
- if (annotations && annotations.length > 0) {
173
- annotationsData = annotations;
174
- highlightBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg> ${annotations.length} highlighted`;
1399
+ backdrop.querySelector("#cf-remove-screenshot")?.addEventListener("click", () => {
1400
+ collapseModal(cfg, opts);
1401
+ });
1402
+ backdrop.querySelector("#cf-annotate-btn")?.addEventListener("click", () => {
1403
+ openAnnotationEditor(cfg, opts);
1404
+ });
1405
+ backdrop.querySelector("#cf-submit")?.addEventListener("click", () => {
1406
+ const desc = backdrop.querySelector("#cf-desc")?.value;
1407
+ const name = backdrop.querySelector("#cf-name")?.value;
1408
+ const email = backdrop.querySelector("#cf-email")?.value;
1409
+ if (!desc?.trim()) {
1410
+ const descEl = backdrop.querySelector("#cf-desc");
1411
+ if (descEl) {
1412
+ descEl.style.borderColor = "#e74c3c";
1413
+ descEl.focus();
175
1414
  }
176
- } catch {
1415
+ return;
177
1416
  }
178
- form.style.display = "flex";
179
- });
180
- submitBtn?.addEventListener("click", () => {
181
- const title = container.querySelector("#cf-title")?.value;
182
- const desc = container.querySelector("#cf-desc")?.value;
183
- const type = container.querySelector("#cf-type")?.value;
184
- const priority = container.querySelector("#cf-priority")?.value;
185
- if (!title?.trim()) return;
186
- onSubmitCallback?.({ title, description: desc, type, priority, screenshot: screenshotDataUrl || void 0, annotations: annotationsData.length > 0 ? annotationsData : void 0 });
187
- container.querySelector("#cf-title").value = "";
188
- container.querySelector("#cf-desc").value = "";
189
- screenshotDataUrl = null;
190
- annotationsData = [];
191
- if (screenshotPreview) screenshotPreview.style.display = "none";
192
- form.style.display = "none";
193
- showToast("Feedback sent!");
1417
+ onSubmitCallback?.({
1418
+ title: desc.trim().slice(0, 100),
1419
+ description: desc.trim(),
1420
+ type: "BUG",
1421
+ priority: "MEDIUM",
1422
+ screenshot: annotatedImageUrl || screenshotDataUrl || void 0,
1423
+ annotations: annotationsData.length > 0 ? annotationsData : void 0,
1424
+ reporter_name: name?.trim() || void 0,
1425
+ reporter_email: email?.trim() || void 0
1426
+ });
1427
+ closeModal();
1428
+ showToast("Rapport envoy\xE9 avec succ\xE8s !");
194
1429
  });
195
1430
  }
196
1431
  function unmountWidget() {
1432
+ closeModal();
197
1433
  if (container) {
198
1434
  container.remove();
199
1435
  container = null;
@@ -202,10 +1438,273 @@ function unmountWidget() {
202
1438
  function showToast(msg) {
203
1439
  const toast = document.createElement("div");
204
1440
  toast.textContent = msg;
205
- toast.style.cssText = "position:fixed;bottom:80px;right:20px;background:#10b981;color:#fff;padding:8px 16px;border-radius:8px;font-size:13px;z-index:100001;font-family:system-ui;box-shadow:0 2px 8px rgba(0,0,0,.15);";
1441
+ toast.style.cssText = 'position:fixed;bottom:80px;right:20px;background:#10b981;color:#fff;padding:10px 20px;border-radius:10px;font-size:14px;z-index:100010;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.18);animation:cf-toast-in .3s ease;';
206
1442
  document.body.appendChild(toast);
1443
+ setTimeout(() => {
1444
+ toast.style.opacity = "0";
1445
+ toast.style.transition = "opacity .3s";
1446
+ }, 2500);
207
1447
  setTimeout(() => toast.remove(), 3e3);
208
1448
  }
1449
+ function injectStyles(cfg) {
1450
+ const style = document.createElement("style");
1451
+ style.id = "cf-widget-styles";
1452
+ style.textContent = `
1453
+ @keyframes cf-toast-in { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
1454
+ @keyframes cf-modal-in { from { opacity:0; transform:scale(.96) translateY(8px); } to { opacity:1; transform:scale(1) translateY(0); } }
1455
+
1456
+ #cf-trigger {
1457
+ position:fixed;
1458
+ ${getPositionCSS(cfg.position)}
1459
+ z-index:100000;
1460
+ background:${cfg.color};
1461
+ color:#fff;
1462
+ border:none;
1463
+ border-radius:50px;
1464
+ padding:11px 20px;
1465
+ font-size:14px;
1466
+ font-weight:500;
1467
+ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
1468
+ cursor:pointer;
1469
+ box-shadow:0 4px 16px rgba(0,0,0,.18);
1470
+ display:flex;
1471
+ align-items:center;
1472
+ gap:8px;
1473
+ transition:transform .15s,box-shadow .15s;
1474
+ }
1475
+ #cf-trigger:hover { transform:scale(1.04); box-shadow:0 6px 24px rgba(0,0,0,.22); }
1476
+ #cf-trigger svg { width:18px; height:18px; }
1477
+
1478
+ #cf-modal-backdrop {
1479
+ position:fixed;
1480
+ top:0;left:0;right:0;bottom:0;
1481
+ background:rgba(0,0,0,.45);
1482
+ z-index:100001;
1483
+ display:flex;
1484
+ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
1485
+ }
1486
+ #cf-modal-backdrop.cf-backdrop--center {
1487
+ align-items:center;
1488
+ justify-content:center;
1489
+ }
1490
+ #cf-modal-backdrop.cf-backdrop--corner {
1491
+ padding:20px;
1492
+ }
1493
+ #cf-modal-backdrop.cf-pos-bottom-right {
1494
+ align-items:flex-end;
1495
+ justify-content:flex-end;
1496
+ }
1497
+ #cf-modal-backdrop.cf-pos-bottom-left {
1498
+ align-items:flex-end;
1499
+ justify-content:flex-start;
1500
+ }
1501
+ #cf-modal-backdrop.cf-pos-top-right {
1502
+ align-items:flex-start;
1503
+ justify-content:flex-end;
1504
+ }
1505
+ #cf-modal-backdrop.cf-pos-top-left {
1506
+ align-items:flex-start;
1507
+ justify-content:flex-start;
1508
+ }
1509
+
1510
+ .cf-modal {
1511
+ background:#fff;
1512
+ border-radius:16px;
1513
+ box-shadow:0 24px 80px rgba(0,0,0,.25);
1514
+ overflow:hidden;
1515
+ animation:cf-modal-in .25s ease;
1516
+ display:flex;
1517
+ flex-direction:column;
1518
+ max-height:90vh;
1519
+ }
1520
+ .cf-modal--compact { width:420px; }
1521
+ .cf-modal--expanded { width:min(1100px,92vw); flex-direction:column; }
1522
+
1523
+ .cf-modal-header {
1524
+ display:flex;
1525
+ align-items:center;
1526
+ justify-content:space-between;
1527
+ padding:20px 24px 16px;
1528
+ border-bottom:1px solid #f0f0f0;
1529
+ }
1530
+ .cf-modal-header h2 {
1531
+ margin:0;
1532
+ font-size:20px;
1533
+ font-weight:700;
1534
+ color:#1a1a2e;
1535
+ }
1536
+ .cf-modal-header .cf-logo {
1537
+ width:28px;
1538
+ height:28px;
1539
+ color:${cfg.color};
1540
+ }
1541
+
1542
+ .cf-modal-body { display:flex; flex:1; overflow:hidden; }
1543
+
1544
+ .cf-screenshot-panel {
1545
+ flex:1;
1546
+ min-width:0;
1547
+ background:#1a1a2e;
1548
+ display:flex;
1549
+ flex-direction:column;
1550
+ position:relative;
1551
+ }
1552
+ .cf-screenshot-panel img {
1553
+ width:100%;
1554
+ height:100%;
1555
+ object-fit:contain;
1556
+ max-height:60vh;
1557
+ }
1558
+ .cf-screenshot-tools {
1559
+ position:absolute;
1560
+ bottom:16px;
1561
+ left:50%;
1562
+ transform:translateX(-50%);
1563
+ display:flex;
1564
+ gap:8px;
1565
+ }
1566
+ .cf-screenshot-tools button {
1567
+ padding:8px 18px;
1568
+ border-radius:8px;
1569
+ font-size:13px;
1570
+ font-weight:500;
1571
+ cursor:pointer;
1572
+ font-family:inherit;
1573
+ border:none;
1574
+ transition:all .15s;
1575
+ }
1576
+ .cf-tool-highlight {
1577
+ background:${cfg.color};
1578
+ color:#fff;
1579
+ }
1580
+ .cf-tool-highlight:hover { opacity:.9; }
1581
+ .cf-tool-mask {
1582
+ background:#fff;
1583
+ color:#1a1a2e;
1584
+ border:1px solid #e0e0e0 !important;
1585
+ }
1586
+ .cf-tool-mask:hover { background:#f5f5f5; }
1587
+
1588
+ .cf-form-panel {
1589
+ width:100%;
1590
+ padding:20px 24px;
1591
+ display:flex;
1592
+ flex-direction:column;
1593
+ gap:14px;
1594
+ overflow-y:auto;
1595
+ }
1596
+ .cf-modal--expanded .cf-form-panel {
1597
+ width:340px;
1598
+ flex-shrink:0;
1599
+ border-left:1px solid #f0f0f0;
1600
+ }
1601
+
1602
+ .cf-field label {
1603
+ display:block;
1604
+ font-size:13px;
1605
+ font-weight:500;
1606
+ color:#1a1a2e;
1607
+ margin-bottom:6px;
1608
+ }
1609
+ .cf-field label span { font-weight:400; color:#999; }
1610
+ .cf-field input, .cf-field textarea {
1611
+ width:100%;
1612
+ padding:10px 14px;
1613
+ border:1px solid #e0e0e0;
1614
+ border-radius:10px;
1615
+ font-size:14px;
1616
+ font-family:inherit;
1617
+ box-sizing:border-box;
1618
+ outline:none;
1619
+ transition:border-color .15s,box-shadow .15s;
1620
+ color:#1a1a2e;
1621
+ background:#fff;
1622
+ }
1623
+ .cf-field input::placeholder, .cf-field textarea::placeholder { color:#bbb; }
1624
+ .cf-field input:focus, .cf-field textarea:focus {
1625
+ border-color:${cfg.color};
1626
+ box-shadow:0 0 0 3px ${cfg.color}18;
1627
+ }
1628
+ .cf-field textarea { resize:vertical; min-height:100px; }
1629
+
1630
+ .cf-screenshot-action {
1631
+ width:100%;
1632
+ padding:12px;
1633
+ border:1px solid #e0e0e0;
1634
+ border-radius:10px;
1635
+ background:#fff;
1636
+ font-size:14px;
1637
+ color:#1a1a2e;
1638
+ cursor:pointer;
1639
+ font-family:inherit;
1640
+ transition:all .15s;
1641
+ text-align:center;
1642
+ }
1643
+ .cf-screenshot-action:hover { background:#f8f8f8; border-color:#ccc; }
1644
+
1645
+ .cf-remove-screenshot {
1646
+ width:100%;
1647
+ padding:10px;
1648
+ border:1px solid #e0e0e0;
1649
+ border-radius:10px;
1650
+ background:#fff;
1651
+ font-size:13px;
1652
+ color:#666;
1653
+ cursor:pointer;
1654
+ font-family:inherit;
1655
+ transition:all .15s;
1656
+ text-align:center;
1657
+ }
1658
+ .cf-remove-screenshot:hover { background:#fff0f0; color:#e74c3c; border-color:#e74c3c; }
1659
+
1660
+ .cf-submit-btn {
1661
+ width:100%;
1662
+ padding:13px;
1663
+ border:none;
1664
+ border-radius:10px;
1665
+ background:${cfg.color};
1666
+ color:#fff;
1667
+ font-size:15px;
1668
+ font-weight:600;
1669
+ cursor:pointer;
1670
+ font-family:inherit;
1671
+ transition:all .15s;
1672
+ }
1673
+ .cf-submit-btn:hover { opacity:.92; transform:translateY(-1px); box-shadow:0 4px 12px ${cfg.color}40; }
1674
+ .cf-submit-btn:disabled { opacity:.5; cursor:not-allowed; transform:none; }
1675
+
1676
+ .cf-cancel-btn {
1677
+ width:100%;
1678
+ padding:11px;
1679
+ border:1px solid #e0e0e0;
1680
+ border-radius:10px;
1681
+ background:#fff;
1682
+ color:#1a1a2e;
1683
+ font-size:14px;
1684
+ font-weight:500;
1685
+ cursor:pointer;
1686
+ font-family:inherit;
1687
+ transition:all .15s;
1688
+ text-align:center;
1689
+ }
1690
+ .cf-cancel-btn:hover { background:#f8f8f8; }
1691
+
1692
+ .cf-footer {
1693
+ text-align:center;
1694
+ padding:12px;
1695
+ border-top:1px solid #f0f0f0;
1696
+ font-size:11px;
1697
+ color:#bbb;
1698
+ }
1699
+ .cf-footer a {
1700
+ color:#999;
1701
+ text-decoration:none;
1702
+ font-weight:500;
1703
+ }
1704
+ .cf-footer a:hover { color:${cfg.color}; }
1705
+ `;
1706
+ document.head.appendChild(style);
1707
+ }
209
1708
  function getPositionCSS(pos) {
210
1709
  switch (pos) {
211
1710
  case "bottom-left":
@@ -214,83 +1713,81 @@ function getPositionCSS(pos) {
214
1713
  return "top:20px;right:20px;";
215
1714
  case "top-left":
216
1715
  return "top:20px;left:20px;";
1716
+ case "center":
1717
+ return "bottom:20px;left:50%;transform:translateX(-50%);";
217
1718
  default:
218
1719
  return "bottom:20px;right:20px;";
219
1720
  }
220
1721
  }
221
- function getFormPositionCSS(pos) {
222
- switch (pos) {
223
- case "bottom-left":
224
- return "bottom:70px;left:20px;";
225
- case "top-right":
226
- return "top:70px;right:20px;";
227
- case "top-left":
228
- return "top:70px;left:20px;";
229
- default:
230
- return "bottom:70px;right:20px;";
231
- }
232
- }
233
- function getWidgetHTML(cfg) {
234
- const posBtn = getPositionCSS(cfg.position);
235
- const posForm = getFormPositionCSS(cfg.position);
1722
+ function getTriggerHTML(cfg) {
236
1723
  return `
237
- <style>
238
- #cf-trigger{position:fixed;${posBtn}z-index:100000;background:${cfg.color};color:#fff;border:none;border-radius:50px;padding:10px 18px;font-size:13px;font-family:system-ui,-apple-system,sans-serif;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,.15);display:flex;align-items:center;gap:6px;transition:transform .15s}
239
- #cf-trigger:hover{transform:scale(1.05)}
240
- #cf-trigger svg{width:16px;height:16px}
241
- #cf-form{position:fixed;${posForm}z-index:100000;background:#fff;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.12);width:360px;padding:16px;font-family:system-ui,-apple-system,sans-serif;display:none;flex-direction:column;gap:10px}
242
- #cf-form h3{margin:0;font-size:15px;font-weight:600;color:#111}
243
- #cf-form input,#cf-form textarea,#cf-form select{width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box;outline:none;transition:border .15s}
244
- #cf-form input:focus,#cf-form textarea:focus,#cf-form select:focus{border-color:${cfg.color}}
245
- #cf-form textarea{resize:vertical;min-height:60px}
246
- .cf-row{display:flex;gap:8px}
247
- .cf-row select{flex:1}
248
- .cf-tools{display:flex;gap:6px}
249
- .cf-tool-btn{display:flex;align-items:center;gap:4px;padding:6px 10px;border:1px solid #e2e8f0;border-radius:6px;background:#f8fafc;color:#475569;font-size:11px;cursor:pointer;font-family:inherit;transition:all .15s}
250
- .cf-tool-btn:hover{background:#f1f5f9;border-color:#cbd5e1}
251
- .cf-tool-btn svg{width:14px;height:14px}
252
- #cf-submit{background:${cfg.color};color:#fff;border:none;border-radius:8px;padding:9px;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .15s}
253
- #cf-submit:hover{opacity:.9}
254
- #cf-close{position:absolute;top:10px;right:12px;background:none;border:none;cursor:pointer;font-size:18px;color:#94a3b8;line-height:1}
255
- #cf-screenshot-preview{display:none;border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;max-height:120px}
256
- #cf-screenshot-preview img{width:100%;height:auto;display:block}
257
- </style>
258
- <button id="cf-trigger">
259
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
260
- ${cfg.text}
261
- </button>
262
- <div id="cf-form">
263
- <button id="cf-close">&times;</button>
264
- <h3>Report Feedback</h3>
265
- <input id="cf-title" type="text" placeholder="Title *" />
266
- <textarea id="cf-desc" placeholder="Description (optional)"></textarea>
267
- <div class="cf-row">
268
- <select id="cf-type">
269
- <option value="BUG">Bug</option>
270
- <option value="FEATURE">Feature</option>
271
- <option value="IMPROVEMENT">Improvement</option>
272
- <option value="QUESTION">Question</option>
273
- </select>
274
- <select id="cf-priority">
275
- <option value="LOW">Low</option>
276
- <option value="MEDIUM" selected>Medium</option>
277
- <option value="HIGH">High</option>
278
- <option value="CRITICAL">Critical</option>
279
- </select>
280
- </div>
281
- <div class="cf-tools">
282
- <button type="button" class="cf-tool-btn" id="cf-screenshot">
283
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>
284
- Screenshot
285
- </button>
286
- <button type="button" class="cf-tool-btn" id="cf-highlight">
287
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
288
- Highlight
1724
+ <button id="cf-trigger">
1725
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
1726
+ ${cfg.text}
289
1727
  </button>
290
- </div>
291
- <div id="cf-screenshot-preview"><img src="" alt="screenshot" /></div>
292
- <button id="cf-submit">Send Feedback</button>
293
- </div>`;
1728
+ `;
1729
+ }
1730
+ function getCheckflowLogo() {
1731
+ return `<svg class="cf-logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12C2 6.5 6.5 2 12 2a10 10 0 0 1 8 4"/><path d="M5 19.5C5.5 18 6 15 6 12c0-.7.12-1.37.34-2"/><path d="M17.29 21.02c.12-.6.43-2.3.5-3.02"/><path d="M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4"/><path d="M8.65 22c.21-.66.45-1.32.57-2"/><path d="M14 13.12c0 2.38 0 6.38-1 8.88"/><path d="M2 16h.01"/><path d="M21.8 16c.2-2 .131-5.354 0-6"/><path d="M9 6.8a6 6 0 0 1 9 5.2v2"/></svg>`;
1732
+ }
1733
+ function getModalHTML(cfg, expanded) {
1734
+ const modalClass = expanded ? "cf-modal cf-modal--expanded" : "cf-modal cf-modal--compact";
1735
+ const formFields = `
1736
+ <div class="cf-field">
1737
+ <label>Nom <span>(obligatoire)</span></label>
1738
+ <input id="cf-name" type="text" placeholder="Votre nom" />
1739
+ </div>
1740
+ <div class="cf-field">
1741
+ <label>Email <span>(obligatoire)</span></label>
1742
+ <input id="cf-email" type="email" placeholder="votre.email@exemple.com" />
1743
+ </div>
1744
+ <div class="cf-field">
1745
+ <label>Description <span>(obligatoire)</span></label>
1746
+ <textarea id="cf-desc" placeholder="Quel est le probl\xE8me ? Que vous attendiez-vous \xE0 voir ?"></textarea>
1747
+ </div>
1748
+ `;
1749
+ if (expanded && screenshotDataUrl) {
1750
+ const displayImg = annotatedImageUrl || screenshotDataUrl;
1751
+ const annotLabel = annotationsData.length > 0 ? `Annoter (${annotationsData.length})` : "Annoter";
1752
+ return `
1753
+ <div class="${modalClass}">
1754
+ <div class="cf-modal-header">
1755
+ <h2>Envoyer le rapport</h2>
1756
+ ${getCheckflowLogo()}
1757
+ </div>
1758
+ <div class="cf-modal-body">
1759
+ <div class="cf-screenshot-panel">
1760
+ <img id="cf-screenshot-img" src="${displayImg}" alt="Capture d'\xE9cran" />
1761
+ <div class="cf-screenshot-tools">
1762
+ <button class="cf-tool-highlight" id="cf-annotate-btn">${annotLabel}</button>
1763
+ </div>
1764
+ </div>
1765
+ <div class="cf-form-panel">
1766
+ ${formFields}
1767
+ <button class="cf-remove-screenshot" id="cf-remove-screenshot">Supprimer la capture d'\xE9cran</button>
1768
+ <button class="cf-submit-btn" id="cf-submit">Envoyer le rapport</button>
1769
+ <button class="cf-cancel-btn" id="cf-cancel">Annuler</button>
1770
+ </div>
1771
+ </div>
1772
+ <div class="cf-footer">Powered by <a href="https://checkflow.space" target="_blank" rel="noopener">Checkflow</a></div>
1773
+ </div>
1774
+ `;
1775
+ }
1776
+ return `
1777
+ <div class="${modalClass}">
1778
+ <div class="cf-modal-header">
1779
+ <h2>Envoyer le rapport</h2>
1780
+ ${getCheckflowLogo()}
1781
+ </div>
1782
+ <div class="cf-form-panel">
1783
+ ${formFields}
1784
+ <button class="cf-screenshot-action" id="cf-screenshot-btn">Ajouter une capture d'\xE9cran</button>
1785
+ <button class="cf-submit-btn" id="cf-submit">Envoyer le rapport</button>
1786
+ <button class="cf-cancel-btn" id="cf-cancel">Annuler</button>
1787
+ </div>
1788
+ <div class="cf-footer">Powered by <a href="https://checkflow.space" target="_blank" rel="noopener">Checkflow</a></div>
1789
+ </div>
1790
+ `;
294
1791
  }
295
1792
 
296
1793
  // src/index.ts
@@ -306,10 +1803,7 @@ function init(cfg) {
306
1803
  };
307
1804
  if (!config.enabled) return;
308
1805
  installInterceptors();
309
- if (typeof window !== "undefined") {
310
- loadHtml2Canvas().catch(() => {
311
- });
312
- }
1806
+ installNetworkInterceptor();
313
1807
  if (typeof window !== "undefined" && config.widget?.showOnInit !== false) {
314
1808
  mountWidget(
315
1809
  config.widget,
@@ -320,12 +1814,13 @@ function init(cfg) {
320
1814
  type: data.type,
321
1815
  priority: data.priority,
322
1816
  screenshot_data: data.screenshot,
323
- annotations: data.annotations
1817
+ annotations: data.annotations,
1818
+ reporter_name: data.reporter_name,
1819
+ reporter_email: data.reporter_email
324
1820
  });
325
1821
  },
326
1822
  {
327
- onScreenshot: () => captureScreenshot(),
328
- onHighlight: () => startHighlighting()
1823
+ onScreenshot: () => captureScreenshot()
329
1824
  }
330
1825
  );
331
1826
  }
@@ -338,6 +1833,7 @@ async function sendFeedback(data) {
338
1833
  const perf = typeof window !== "undefined" ? collectPerformance() : void 0;
339
1834
  const consoleLogs = getConsoleLogs();
340
1835
  const jsErrors = getJavascriptErrors();
1836
+ const networkLogs = getNetworkLogs();
341
1837
  const payload = {
342
1838
  ...data,
343
1839
  ...context,
@@ -345,6 +1841,7 @@ async function sendFeedback(data) {
345
1841
  performance_metrics: perf,
346
1842
  console_logs: consoleLogs.length > 0 ? consoleLogs : void 0,
347
1843
  javascript_errors: jsErrors.length > 0 ? jsErrors : void 0,
1844
+ network_logs: networkLogs.length > 0 ? networkLogs : void 0,
348
1845
  sdk_version: SDK_VERSION
349
1846
  };
350
1847
  if (data.screenshot_data) {
@@ -353,6 +1850,12 @@ async function sendFeedback(data) {
353
1850
  if (data.annotations && data.annotations.length > 0) {
354
1851
  payload.annotations = data.annotations;
355
1852
  }
1853
+ if (data.reporter_name) {
1854
+ payload.reporter_name = data.reporter_name;
1855
+ }
1856
+ if (data.reporter_email) {
1857
+ payload.reporter_email = data.reporter_email;
1858
+ }
356
1859
  try {
357
1860
  const res = await fetch(`${config.endpoint}/sdk/feedback`, {
358
1861
  method: "POST",
@@ -382,12 +1885,13 @@ function showWidget() {
382
1885
  type: data.type,
383
1886
  priority: data.priority,
384
1887
  screenshot_data: data.screenshot,
385
- annotations: data.annotations
1888
+ annotations: data.annotations,
1889
+ reporter_name: data.reporter_name,
1890
+ reporter_email: data.reporter_email
386
1891
  });
387
1892
  },
388
1893
  {
389
- onScreenshot: () => captureScreenshot(),
390
- onHighlight: () => startHighlighting()
1894
+ onScreenshot: () => captureScreenshot()
391
1895
  }
392
1896
  );
393
1897
  }
@@ -397,6 +1901,7 @@ function hideWidget() {
397
1901
  function destroy() {
398
1902
  unmountWidget();
399
1903
  clearLogs();
1904
+ clearNetworkLogs();
400
1905
  clearScreenshot();
401
1906
  config = null;
402
1907
  }
@@ -406,6 +1911,5 @@ export {
406
1911
  hideWidget,
407
1912
  init,
408
1913
  sendFeedback,
409
- showWidget,
410
- startHighlighting
1914
+ showWidget
411
1915
  };