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