@comty.io/core 0.1.3 → 0.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/README.md +18 -0
- package/comty-sdk.js +1231 -940
- package/package.json +8 -1
- package/src/browser.js +432 -262
- package/src/capture-engine.js +26 -0
- package/src/capture-resources.js +94 -0
- package/src/capture.js +249 -459
- package/src/comments.js +41 -24
- package/src/style.js +360 -165
package/comty-sdk.js
CHANGED
|
@@ -13,7 +13,8 @@ function getComtyStyle() {
|
|
|
13
13
|
--comty-muted: var(--comty-toolbar-muted);
|
|
14
14
|
--comty-accent: #55aaf5;
|
|
15
15
|
--comty-error: #ef7676;
|
|
16
|
-
--comty-outline:
|
|
16
|
+
--comty-outline: var(--comty-toolbar-muted);
|
|
17
|
+
--comty-comment-focus: var(--comty-toolbar-muted);
|
|
17
18
|
--comty-paper: #fff;
|
|
18
19
|
--comty-ink: #171717;
|
|
19
20
|
--comty-border: #434649;
|
|
@@ -37,6 +38,10 @@ function getComtyStyle() {
|
|
|
37
38
|
--comty-magnifier-gray: #999;
|
|
38
39
|
--comty-magnifier-shadow: 0 .0625rem .375rem rgb(0 0 0 / .5);
|
|
39
40
|
--comty-property-width: 16rem;
|
|
41
|
+
--comty-inspector-width: 18rem;
|
|
42
|
+
--comty-inspector-label-width: 5rem;
|
|
43
|
+
--comty-inspector-row-height: 1.5rem;
|
|
44
|
+
--comty-copy-size: 1.5rem;
|
|
40
45
|
--comty-property-swatch: 2rem;
|
|
41
46
|
--comty-property-font-size: .6875rem;
|
|
42
47
|
--comty-property-row-height: 1.375rem;
|
|
@@ -50,7 +55,6 @@ function getComtyStyle() {
|
|
|
50
55
|
--comty-dialog-ink: var(--comty-foreground);
|
|
51
56
|
--comty-dialog-soft: var(--comty-muted);
|
|
52
57
|
--comty-dialog-field: var(--comty-field-surface);
|
|
53
|
-
--comty-dialog-line: var(--comty-border);
|
|
54
58
|
--comty-dialog-backdrop: rgb(0 0 0 / .4);
|
|
55
59
|
--comty-dialog-radius: 2rem;
|
|
56
60
|
--comty-dialog-width: 32rem;
|
|
@@ -325,11 +329,11 @@ function getComtyStyle() {
|
|
|
325
329
|
.comty-pin {
|
|
326
330
|
position: fixed;
|
|
327
331
|
z-index: 2147483645;
|
|
328
|
-
width:
|
|
329
|
-
height:
|
|
332
|
+
width: 28px;
|
|
333
|
+
height: 28px;
|
|
330
334
|
padding: 0;
|
|
331
335
|
border: none;
|
|
332
|
-
border-radius: 999px 999px 999px
|
|
336
|
+
border-radius: 999px 999px 999px 10px;
|
|
333
337
|
background: #0078FF;
|
|
334
338
|
color: white;
|
|
335
339
|
display: grid;
|
|
@@ -339,17 +343,17 @@ function getComtyStyle() {
|
|
|
339
343
|
transform: translate(-50%, -50%);
|
|
340
344
|
appearance: none;
|
|
341
345
|
font-family: sans-serif;
|
|
342
|
-
filter: drop-shadow(0 8px
|
|
346
|
+
filter: drop-shadow(0 8px 16px rgba(0, 120, 255, .28));
|
|
343
347
|
display: none;
|
|
344
348
|
overflow: visible;
|
|
345
349
|
}
|
|
346
350
|
|
|
347
|
-
#comty-root > .comty-temp-pin { pointer-events: none; }
|
|
351
|
+
#comty-root > .comty-temp-pin { pointer-events: none; z-index: 2147483647; }
|
|
348
352
|
|
|
349
353
|
.comty-pin-icon {
|
|
350
354
|
display: block;
|
|
351
|
-
width:
|
|
352
|
-
height:
|
|
355
|
+
width: 16px;
|
|
356
|
+
height: 16px;
|
|
353
357
|
pointer-events: none;
|
|
354
358
|
}
|
|
355
359
|
|
|
@@ -358,11 +362,11 @@ function getComtyStyle() {
|
|
|
358
362
|
top: -4px;
|
|
359
363
|
right: -4px;
|
|
360
364
|
display: inline-flex;
|
|
361
|
-
width:
|
|
362
|
-
height:
|
|
365
|
+
width: 10px;
|
|
366
|
+
height: 10px;
|
|
363
367
|
border: 1px solid rgba(255,255,255,.82);
|
|
364
368
|
border-radius: 999px;
|
|
365
|
-
box-shadow: 0 8px
|
|
369
|
+
box-shadow: 0 8px 16px rgba(15,23,42,.18);
|
|
366
370
|
pointer-events: none;
|
|
367
371
|
}
|
|
368
372
|
|
|
@@ -370,7 +374,7 @@ function getComtyStyle() {
|
|
|
370
374
|
background: var(--comty-surface);
|
|
371
375
|
color: var(--comty-foreground);
|
|
372
376
|
border: 2px solid var(--comty-border);
|
|
373
|
-
box-shadow: 0 2px
|
|
377
|
+
box-shadow: 0 2px 4px rgba(0,0,0,.18);
|
|
374
378
|
filter: none;
|
|
375
379
|
transition:
|
|
376
380
|
border-color 160ms ease,
|
|
@@ -420,14 +424,14 @@ function getComtyStyle() {
|
|
|
420
424
|
background: var(--comty-surface);
|
|
421
425
|
color: var(--comty-foreground);
|
|
422
426
|
border: 0;
|
|
423
|
-
box-shadow: 0
|
|
427
|
+
box-shadow: 0 8px 24px rgba(0,0,0,.2);
|
|
424
428
|
padding: 0;
|
|
425
|
-
border-radius:
|
|
426
|
-
width: min(
|
|
429
|
+
border-radius: 12px;
|
|
430
|
+
width: min(288px, calc(100vw - 24px));
|
|
427
431
|
overflow: hidden;
|
|
428
432
|
box-sizing: border-box;
|
|
429
433
|
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
430
|
-
font-size:
|
|
434
|
+
font-size: 12px;
|
|
431
435
|
display: none;
|
|
432
436
|
animation: comty-bubble-in 200ms cubic-bezier(.23,1,.32,1) forwards;
|
|
433
437
|
transform-origin: top left;
|
|
@@ -455,37 +459,53 @@ function getComtyStyle() {
|
|
|
455
459
|
display: flex;
|
|
456
460
|
align-items: center;
|
|
457
461
|
justify-content: space-between;
|
|
458
|
-
gap:
|
|
459
|
-
min-height:
|
|
460
|
-
padding: 0
|
|
461
|
-
border-bottom: 1px solid var(--comty-border);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
.comty-bubble-title {
|
|
465
|
-
margin: 0;
|
|
466
|
-
color: var(--comty-foreground);
|
|
467
|
-
font-size: 12px;
|
|
468
|
-
font-weight: 600;
|
|
469
|
-
line-height: 1;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
.comty-bubble-header-actions {
|
|
473
|
-
display: flex;
|
|
474
|
-
align-items: center;
|
|
475
|
-
gap: 6px;
|
|
476
|
-
margin-left: auto;
|
|
462
|
+
gap: 10px;
|
|
463
|
+
min-height: 1.5rem;
|
|
464
|
+
padding: .625rem .625rem 0;
|
|
477
465
|
}
|
|
478
466
|
|
|
479
467
|
.comty-bubble-content {
|
|
480
468
|
flex: 1;
|
|
481
469
|
color: var(--comty-foreground);
|
|
482
|
-
font-size:
|
|
470
|
+
font-size: .75rem;
|
|
483
471
|
line-height: 1.45;
|
|
484
472
|
overflow-wrap: anywhere;
|
|
485
473
|
cursor: text;
|
|
486
474
|
white-space: pre-wrap;
|
|
487
475
|
}
|
|
488
476
|
|
|
477
|
+
.comty-bubble-content a {
|
|
478
|
+
color: var(--comty-comment-focus);
|
|
479
|
+
text-decoration: underline;
|
|
480
|
+
text-underline-offset: .15em;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.comty-bubble-content a:focus-visible {
|
|
484
|
+
outline: .125rem solid var(--comty-comment-focus);
|
|
485
|
+
outline-offset: .125rem;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.comty-comment-link {
|
|
489
|
+
display: inline-block;
|
|
490
|
+
box-sizing: border-box;
|
|
491
|
+
max-width: 100%;
|
|
492
|
+
min-width: 0;
|
|
493
|
+
overflow: hidden;
|
|
494
|
+
padding: .375rem .625rem;
|
|
495
|
+
border-radius: .375rem;
|
|
496
|
+
background: var(--comty-field-surface);
|
|
497
|
+
color: var(--comty-foreground);
|
|
498
|
+
font: .75rem/1.25 var(--comty-font);
|
|
499
|
+
text-decoration: none;
|
|
500
|
+
text-overflow: ellipsis;
|
|
501
|
+
white-space: nowrap;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.comty-thread-item .comty-comment-link,
|
|
505
|
+
.comty-capture-annotation .comty-comment-link { margin-top: .5rem; }
|
|
506
|
+
.comty-comment-link:hover { text-decoration: underline; }
|
|
507
|
+
.comty-comment-link:focus-visible { outline: .125rem solid var(--comty-comment-focus); outline-offset: .125rem; }
|
|
508
|
+
|
|
489
509
|
.comty-bubble-content-editor {
|
|
490
510
|
display: block;
|
|
491
511
|
flex: 1;
|
|
@@ -497,7 +517,7 @@ function getComtyStyle() {
|
|
|
497
517
|
background: transparent;
|
|
498
518
|
color: var(--comty-foreground);
|
|
499
519
|
cursor: text;
|
|
500
|
-
font:
|
|
520
|
+
font: .75rem/1.45 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
501
521
|
outline: none;
|
|
502
522
|
resize: none;
|
|
503
523
|
overflow: hidden;
|
|
@@ -507,15 +527,15 @@ function getComtyStyle() {
|
|
|
507
527
|
}
|
|
508
528
|
|
|
509
529
|
.comty-thread {
|
|
510
|
-
padding:
|
|
530
|
+
padding: .625rem;
|
|
511
531
|
}
|
|
512
532
|
|
|
513
533
|
.comty-thread-list {
|
|
514
534
|
display: grid;
|
|
515
535
|
width: 100%;
|
|
516
536
|
min-width: 0;
|
|
517
|
-
gap:
|
|
518
|
-
max-height: min(
|
|
537
|
+
gap: 16px;
|
|
538
|
+
max-height: min(288px, calc(100vh - 120px));
|
|
519
539
|
overflow-x: hidden;
|
|
520
540
|
overflow-y: auto;
|
|
521
541
|
overscroll-behavior: contain;
|
|
@@ -528,9 +548,9 @@ function getComtyStyle() {
|
|
|
528
548
|
display: grid;
|
|
529
549
|
width: 100%;
|
|
530
550
|
min-width: 0;
|
|
531
|
-
grid-template-columns:
|
|
551
|
+
grid-template-columns: 1.5rem minmax(0, 1fr);
|
|
532
552
|
align-items: start;
|
|
533
|
-
gap:
|
|
553
|
+
gap: 10px;
|
|
534
554
|
}
|
|
535
555
|
|
|
536
556
|
.comty-thread-item-body {
|
|
@@ -545,15 +565,15 @@ function getComtyStyle() {
|
|
|
545
565
|
display: flex;
|
|
546
566
|
align-items: center;
|
|
547
567
|
min-width: 0;
|
|
548
|
-
min-height:
|
|
549
|
-
gap:
|
|
568
|
+
min-height: 16px;
|
|
569
|
+
gap: 6px;
|
|
550
570
|
}
|
|
551
571
|
|
|
552
572
|
.comty-comment-author-name {
|
|
553
573
|
min-width: 0;
|
|
554
574
|
overflow: hidden;
|
|
555
575
|
color: var(--comty-foreground);
|
|
556
|
-
font-size:
|
|
576
|
+
font-size: .75rem;
|
|
557
577
|
font-weight: 600;
|
|
558
578
|
line-height: 1.25;
|
|
559
579
|
text-overflow: ellipsis;
|
|
@@ -563,7 +583,7 @@ function getComtyStyle() {
|
|
|
563
583
|
.comty-comment-time {
|
|
564
584
|
flex: 0 0 auto;
|
|
565
585
|
color: var(--comty-muted);
|
|
566
|
-
font-size:
|
|
586
|
+
font-size: .6875rem;
|
|
567
587
|
font-weight: 400;
|
|
568
588
|
line-height: 1.25;
|
|
569
589
|
}
|
|
@@ -571,13 +591,13 @@ function getComtyStyle() {
|
|
|
571
591
|
.comty-comment-avatar {
|
|
572
592
|
display: grid;
|
|
573
593
|
place-items: center;
|
|
574
|
-
width:
|
|
575
|
-
height:
|
|
594
|
+
width: 1.5rem;
|
|
595
|
+
height: 1.5rem;
|
|
576
596
|
overflow: hidden;
|
|
577
597
|
border-radius: 50%;
|
|
578
598
|
background: var(--comty-field-surface);
|
|
579
599
|
color: var(--comty-foreground);
|
|
580
|
-
font-size:
|
|
600
|
+
font-size: 8px;
|
|
581
601
|
font-weight: 600;
|
|
582
602
|
line-height: 1;
|
|
583
603
|
}
|
|
@@ -589,8 +609,8 @@ function getComtyStyle() {
|
|
|
589
609
|
}
|
|
590
610
|
|
|
591
611
|
.comty-pin-avatar {
|
|
592
|
-
width:
|
|
593
|
-
height:
|
|
612
|
+
width: 24px;
|
|
613
|
+
height: 24px;
|
|
594
614
|
}
|
|
595
615
|
|
|
596
616
|
.comty-thread-item .comty-bubble-content,
|
|
@@ -605,12 +625,12 @@ function getComtyStyle() {
|
|
|
605
625
|
.comty-icon-button {
|
|
606
626
|
display: inline-grid;
|
|
607
627
|
place-items: center;
|
|
608
|
-
width:
|
|
609
|
-
height:
|
|
628
|
+
width: 24px;
|
|
629
|
+
height: 24px;
|
|
610
630
|
flex: 0 0 auto;
|
|
611
631
|
padding: 0;
|
|
612
632
|
border: 0;
|
|
613
|
-
border-radius:
|
|
633
|
+
border-radius: 4px;
|
|
614
634
|
background: transparent;
|
|
615
635
|
color: var(--comty-foreground);
|
|
616
636
|
cursor: pointer;
|
|
@@ -640,24 +660,24 @@ function getComtyStyle() {
|
|
|
640
660
|
}
|
|
641
661
|
|
|
642
662
|
.comty-icon-button svg {
|
|
643
|
-
width:
|
|
644
|
-
height:
|
|
663
|
+
width: 16px;
|
|
664
|
+
height: 16px;
|
|
645
665
|
pointer-events: none;
|
|
646
666
|
}
|
|
647
667
|
|
|
648
668
|
.comty-status-trigger {
|
|
649
669
|
display: inline-flex;
|
|
650
670
|
align-items: center;
|
|
651
|
-
gap:
|
|
652
|
-
min-height:
|
|
671
|
+
gap: 4px;
|
|
672
|
+
min-height: 24px;
|
|
653
673
|
padding: 0 8px;
|
|
654
674
|
border: 0;
|
|
655
|
-
border-radius:
|
|
675
|
+
border-radius: 4px;
|
|
656
676
|
background: transparent;
|
|
657
677
|
color: var(--comty-foreground);
|
|
658
678
|
cursor: pointer;
|
|
659
679
|
appearance: none;
|
|
660
|
-
font: 600
|
|
680
|
+
font: 600 .6875rem/1 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
661
681
|
transition:
|
|
662
682
|
background-color 160ms ease,
|
|
663
683
|
transform 140ms ease-out;
|
|
@@ -686,14 +706,14 @@ function getComtyStyle() {
|
|
|
686
706
|
}
|
|
687
707
|
|
|
688
708
|
.comty-thread-more {
|
|
689
|
-
width:
|
|
690
|
-
height:
|
|
691
|
-
margin:
|
|
709
|
+
width: 20px;
|
|
710
|
+
height: 20px;
|
|
711
|
+
margin: 0 0 0 auto;
|
|
692
712
|
}
|
|
693
713
|
|
|
694
714
|
.comty-thread-more svg {
|
|
695
|
-
width:
|
|
696
|
-
height:
|
|
715
|
+
width: 12px;
|
|
716
|
+
height: 12px;
|
|
697
717
|
}
|
|
698
718
|
|
|
699
719
|
.comty-entry-actions-popover {
|
|
@@ -712,38 +732,57 @@ function getComtyStyle() {
|
|
|
712
732
|
|
|
713
733
|
.comty-reply-form {
|
|
714
734
|
display: grid;
|
|
715
|
-
grid-template-columns:
|
|
735
|
+
grid-template-columns: 1.5rem minmax(0, 1fr);
|
|
716
736
|
align-items: center;
|
|
717
|
-
gap:
|
|
718
|
-
margin-top:
|
|
737
|
+
gap: 10px;
|
|
738
|
+
margin-top: 16px;
|
|
719
739
|
}
|
|
720
740
|
|
|
721
741
|
.comty-reply-field {
|
|
722
742
|
display: flex;
|
|
723
743
|
align-items: center;
|
|
724
|
-
min-height:
|
|
725
|
-
padding:
|
|
726
|
-
border-radius:
|
|
744
|
+
min-height: 1.875rem;
|
|
745
|
+
padding: .25rem .25rem .25rem .625rem;
|
|
746
|
+
border-radius: 1.25rem;
|
|
727
747
|
background: var(--comty-field-surface);
|
|
728
748
|
box-sizing: border-box;
|
|
729
|
-
transition: box-shadow 160ms ease;
|
|
749
|
+
transition: border-radius var(--comty-motion-duration) var(--comty-ease-in-out), box-shadow 160ms ease;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.comty-reply-field.comty-reply-expanded { border-radius: .75rem; }
|
|
753
|
+
|
|
754
|
+
.comty-reply-form:has(.comty-reply-expanded) > .comty-comment-avatar {
|
|
755
|
+
align-self: start;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.comty-reply-expanded .comty-reply-button {
|
|
759
|
+
align-self: flex-end;
|
|
730
760
|
}
|
|
731
761
|
|
|
732
762
|
.comty-reply-field:focus-within {
|
|
733
|
-
box-shadow: inset 0 0 0
|
|
763
|
+
box-shadow: inset 0 0 0 .0625rem var(--comty-comment-focus);
|
|
734
764
|
}
|
|
735
765
|
|
|
736
766
|
.comty-reply-input {
|
|
737
767
|
min-width: 0;
|
|
768
|
+
min-height: 0;
|
|
738
769
|
flex: 1;
|
|
739
770
|
padding: 0;
|
|
740
771
|
border: 0;
|
|
772
|
+
height: 1.125rem;
|
|
773
|
+
max-height: 5rem;
|
|
741
774
|
background: transparent;
|
|
742
775
|
color: var(--comty-foreground);
|
|
743
|
-
font:
|
|
776
|
+
font: .6875rem/1.125rem Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
744
777
|
outline: none;
|
|
778
|
+
resize: none;
|
|
779
|
+
overflow-y: hidden;
|
|
780
|
+
box-sizing: border-box;
|
|
781
|
+
transition: height var(--comty-motion-duration) var(--comty-ease-in-out);
|
|
745
782
|
}
|
|
746
783
|
|
|
784
|
+
.comty-reply-expanded .comty-reply-input { overflow-y: auto; }
|
|
785
|
+
|
|
747
786
|
.comty-reply-input::placeholder {
|
|
748
787
|
color: var(--comty-muted);
|
|
749
788
|
}
|
|
@@ -751,14 +790,14 @@ function getComtyStyle() {
|
|
|
751
790
|
.comty-reply-button {
|
|
752
791
|
display: inline-grid;
|
|
753
792
|
place-items: center;
|
|
754
|
-
width:
|
|
755
|
-
height:
|
|
793
|
+
width: 1.375rem;
|
|
794
|
+
height: 1.375rem;
|
|
756
795
|
flex: 0 0 auto;
|
|
757
796
|
padding: 0;
|
|
758
797
|
border: 0;
|
|
759
798
|
border-radius: 50%;
|
|
760
|
-
background:
|
|
761
|
-
color:
|
|
799
|
+
background: var(--comty-foreground);
|
|
800
|
+
color: var(--comty-surface);
|
|
762
801
|
cursor: pointer;
|
|
763
802
|
transition:
|
|
764
803
|
background-color 160ms ease,
|
|
@@ -767,7 +806,7 @@ function getComtyStyle() {
|
|
|
767
806
|
}
|
|
768
807
|
|
|
769
808
|
.comty-reply-button:hover:not(:disabled) {
|
|
770
|
-
background:
|
|
809
|
+
background: var(--comty-muted);
|
|
771
810
|
}
|
|
772
811
|
|
|
773
812
|
.comty-reply-button:active:not(:disabled) {
|
|
@@ -775,13 +814,14 @@ function getComtyStyle() {
|
|
|
775
814
|
}
|
|
776
815
|
|
|
777
816
|
.comty-reply-button:disabled {
|
|
817
|
+
color: var(--comty-muted);
|
|
778
818
|
background: var(--comty-hover-surface);
|
|
779
819
|
cursor: default;
|
|
780
820
|
}
|
|
781
821
|
|
|
782
822
|
.comty-reply-button svg {
|
|
783
|
-
width:
|
|
784
|
-
height:
|
|
823
|
+
width: 12px;
|
|
824
|
+
height: 12px;
|
|
785
825
|
}
|
|
786
826
|
|
|
787
827
|
.comty-reply-form-guest { display: block; }
|
|
@@ -789,20 +829,20 @@ function getComtyStyle() {
|
|
|
789
829
|
.comty-sign-in-reply {
|
|
790
830
|
display: block;
|
|
791
831
|
width: 100%;
|
|
792
|
-
min-height:
|
|
793
|
-
padding: 4px
|
|
832
|
+
min-height: 32px;
|
|
833
|
+
padding: 4px 12px;
|
|
794
834
|
border: 0;
|
|
795
|
-
border-radius:
|
|
835
|
+
border-radius: 12px;
|
|
796
836
|
background: var(--comty-field-surface);
|
|
797
|
-
color:
|
|
837
|
+
color: var(--comty-foreground);
|
|
798
838
|
text-align: left;
|
|
799
|
-
font: 600
|
|
839
|
+
font: 600 12px/1.3 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
800
840
|
cursor: pointer;
|
|
801
841
|
transition: background-color 160ms ease;
|
|
802
842
|
}
|
|
803
843
|
|
|
804
844
|
.comty-sign-in-reply:hover { background: var(--comty-hover-surface); }
|
|
805
|
-
.comty-sign-in-reply:focus-visible { outline:
|
|
845
|
+
.comty-sign-in-reply:focus-visible { outline: .0625rem solid var(--comty-comment-focus); outline-offset: .0625rem; }
|
|
806
846
|
|
|
807
847
|
.comty-hover-bubble {
|
|
808
848
|
width: min(240px, calc(100vw - 32px));
|
|
@@ -828,10 +868,6 @@ function getComtyStyle() {
|
|
|
828
868
|
height: 24px;
|
|
829
869
|
}
|
|
830
870
|
|
|
831
|
-
.comty-hover-bubble .comty-comment-author-name {
|
|
832
|
-
font-size: 12px;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
871
|
.comty-hover-bubble .comty-comment-author {
|
|
836
872
|
min-height: 0;
|
|
837
873
|
}
|
|
@@ -840,11 +876,6 @@ function getComtyStyle() {
|
|
|
840
876
|
font-size: 12px;
|
|
841
877
|
}
|
|
842
878
|
|
|
843
|
-
.comty-hover-bubble .comty-thread-item .comty-bubble-content {
|
|
844
|
-
margin-top: 0;
|
|
845
|
-
font-size: 12px;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
879
|
.comty-auth-backdrop { position: fixed; inset: 0; z-index: 2147483647; display: grid; place-items: center; padding: 1rem; background: var(--comty-dialog-backdrop); font-family: var(--comty-font); }
|
|
849
880
|
.comty-auth-dialog { box-sizing: border-box; width: min(var(--comty-dialog-width), 100%); max-height: calc(100dvh - 2rem); overflow: auto; padding: 1.75rem; border: 0; border-radius: var(--comty-dialog-radius); background: var(--comty-dialog-surface); color: var(--comty-dialog-ink); box-shadow: var(--comty-dialog-shadow); text-align: left; outline: none; }
|
|
850
881
|
.comty-auth-dialog *, .comty-auth-dialog *::before, .comty-auth-dialog *::after { box-sizing: border-box; }
|
|
@@ -855,13 +886,23 @@ function getComtyStyle() {
|
|
|
855
886
|
.comty-auth-header svg { width: 1.25rem; height: 1.25rem; }
|
|
856
887
|
.comty-auth-dialog h2 { margin: 0; color: inherit; font: 600 1.5rem/1.25 var(--comty-font); letter-spacing: -.025em; }
|
|
857
888
|
.comty-auth-dialog p { margin: 1rem 0 0; color: var(--comty-dialog-soft); font: 400 .875rem/1.65 var(--comty-font); }
|
|
858
|
-
.comty-auth-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; margin-top: 1.75rem;
|
|
889
|
+
.comty-auth-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; margin-top: 1.75rem; }
|
|
859
890
|
.comty-auth-actions button { display: inline-flex; align-items: center; justify-content: center; min-height: 2.5rem; margin: 0; padding: .625rem 1rem; border: 0; border-radius: 999rem; background: transparent; color: var(--comty-dialog-soft); font: 500 .875rem/1.25 var(--comty-font); cursor: pointer; }
|
|
860
891
|
.comty-auth-actions button:hover, .comty-auth-close:hover { background: var(--comty-dialog-field); color: var(--comty-dialog-ink); }
|
|
861
892
|
.comty-auth-actions .comty-auth-primary { border-color: transparent; background: var(--comty-dialog-ink); color: var(--comty-dialog-surface); }
|
|
862
893
|
.comty-auth-actions .comty-auth-primary:hover { background: var(--comty-dialog-soft); color: var(--comty-dialog-surface); }
|
|
863
894
|
.comty-auth-dialog button:focus-visible { outline: .125rem solid var(--comty-dialog-ink); outline-offset: .1875rem; }
|
|
864
895
|
.comty-auth-dialog button:disabled { opacity: .5; cursor: wait; }
|
|
896
|
+
.comty-guest-form { display: grid; gap: .5rem; margin-top: 1.5rem; }
|
|
897
|
+
.comty-guest-form label { color: var(--comty-dialog-soft); font: 500 .875rem/1.5 var(--comty-font); }
|
|
898
|
+
.comty-guest-form input { width: 100%; height: 2.75rem; padding: 0 1rem; border: .0625rem solid var(--comty-border); border-radius: .75rem; background: var(--comty-dialog-field); color: var(--comty-dialog-ink); font: 400 .875rem/1.5 var(--comty-font); outline: none; }
|
|
899
|
+
.comty-guest-form input::placeholder { color: var(--comty-dialog-soft); }
|
|
900
|
+
.comty-guest-form input:focus-visible { border-color: var(--comty-dialog-ink); box-shadow: 0 0 0 .0625rem var(--comty-dialog-ink); }
|
|
901
|
+
.comty-guest-form [role="alert"] { margin: 0; color: var(--comty-error); }
|
|
902
|
+
.comty-guest-actions { display: grid; justify-content: stretch; gap: .75rem; margin-top: 1rem; }
|
|
903
|
+
.comty-guest-actions button { width: 100%; }
|
|
904
|
+
.comty-guest-actions [data-comty-login] { background: var(--comty-dialog-field); color: var(--comty-dialog-ink); }
|
|
905
|
+
.comty-auth-or { color: var(--comty-dialog-soft); font: .75rem/1.5 var(--comty-font); text-align: center; }
|
|
865
906
|
.comty-distance-layer { position: fixed; inset: 0; z-index: 1; width: 100%; height: 100%; overflow: visible; pointer-events: none; color: var(--comty-measure); }
|
|
866
907
|
.comty-distance-line, .comty-distance-extension { stroke: currentColor; stroke-width: 1; fill: none; shape-rendering: crispEdges; }
|
|
867
908
|
.comty-distance-extension { stroke-dasharray: 3 3; opacity: .65; }
|
|
@@ -927,8 +968,6 @@ function getComtyStyle() {
|
|
|
927
968
|
justify-content: space-between;
|
|
928
969
|
gap: 8px;
|
|
929
970
|
margin-top: 12px;
|
|
930
|
-
padding-top: 10px;
|
|
931
|
-
border-top: 1px solid var(--comty-border);
|
|
932
971
|
}
|
|
933
972
|
|
|
934
973
|
.comty-status-label {
|
|
@@ -1010,6 +1049,12 @@ function getComtyStyle() {
|
|
|
1010
1049
|
transform-origin: top right;
|
|
1011
1050
|
}
|
|
1012
1051
|
|
|
1052
|
+
.comty-popover.comty-comment-popover {
|
|
1053
|
+
background: var(--comty-field-surface);
|
|
1054
|
+
border: .0625rem solid var(--comty-border);
|
|
1055
|
+
box-shadow: var(--comty-floating-shadow);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1013
1058
|
.comty-popover-item {
|
|
1014
1059
|
display: flex;
|
|
1015
1060
|
align-items: center;
|
|
@@ -1125,11 +1170,11 @@ function getComtyStyle() {
|
|
|
1125
1170
|
.comty-property-row button > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
1126
1171
|
.comty-property-row button[aria-label^="Copy Colour"] > span::before { content: ""; display: inline-block; width: .75rem; height: .75rem; margin-right: .375rem; border: .0625rem solid var(--comty-border); border-radius: .1875rem; background: var(--comty-picked-color); vertical-align: -.0625rem; }
|
|
1127
1172
|
.comty-property-copy-icon { position: relative; flex: 0 0 .75rem; width: .75rem; height: .75rem; color: var(--comty-muted); opacity: .65; }
|
|
1128
|
-
.comty-property-row button:hover .comty-property-copy-icon, .comty-property-row button[data-copied] .comty-property-copy-icon { opacity: 1; }
|
|
1173
|
+
.comty-property-row button:hover .comty-property-copy-icon, .comty-property-row button[data-copied] .comty-property-copy-icon, .comty-inspector-copy:hover .comty-property-copy-icon, .comty-inspector-copy[data-copied] .comty-property-copy-icon { opacity: 1; }
|
|
1129
1174
|
.comty-property-copy-icon svg { position: absolute; inset: 0; width: 100%; height: 100%; transition: opacity 150ms var(--comty-ease-out), transform 150ms var(--comty-ease-out); }
|
|
1130
|
-
.comty-property-copy-icon svg:last-child { opacity: 0; transform: scale(.
|
|
1131
|
-
.comty-property-row button[data-copied] svg:first-child { opacity: 0; transform: scale(.
|
|
1132
|
-
.comty-property-row button[data-copied] svg:last-child { opacity: 1; transform: scale(1); }
|
|
1175
|
+
.comty-property-copy-icon svg:last-child { opacity: 0; transform: scale(.9); }
|
|
1176
|
+
.comty-property-row button[data-copied] svg:first-child, .comty-inspector-copy[data-copied] svg:first-child { opacity: 0; transform: scale(.9); }
|
|
1177
|
+
.comty-property-row button[data-copied] svg:last-child, .comty-inspector-copy[data-copied] svg:last-child { opacity: 1; transform: scale(1); }
|
|
1133
1178
|
@media (prefers-reduced-motion: reduce) { .comty-property-copy-icon svg { transition: none; } }
|
|
1134
1179
|
.comty-property-row button[disabled] { cursor: default; opacity: .5; }
|
|
1135
1180
|
.comty-property-strip { display: flex; align-items: center; justify-content: center; gap: .375rem; }
|
|
@@ -1180,29 +1225,29 @@ function getComtyStyle() {
|
|
|
1180
1225
|
.comty-inspect-gap { --comty-spacing-color: var(--comty-gap-fill); }
|
|
1181
1226
|
.comty-inspect-spacing span { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); padding-inline: .2rem; border-radius: .2rem; background: var(--comty-spacing-color); color: var(--comty-foreground); font: 600 .625rem/1.3 ui-monospace, monospace; white-space: nowrap; }
|
|
1182
1227
|
.comty-inspector {
|
|
1183
|
-
position: fixed; width:
|
|
1184
|
-
padding
|
|
1185
|
-
color: var(--comty-foreground); background: var(--comty-
|
|
1186
|
-
box-shadow: var(--comty-floating-shadow); font:
|
|
1228
|
+
position: fixed; width: var(--comty-inspector-width); max-width: calc(100vw - 1rem); max-height: calc(100vh - 1rem);
|
|
1229
|
+
padding: .25rem; border-radius: .75rem; overflow-y: auto;
|
|
1230
|
+
color: var(--comty-foreground); background: var(--comty-toolbar-group);
|
|
1231
|
+
box-shadow: var(--comty-floating-shadow); font: var(--comty-property-font-size)/1.4 var(--comty-font);
|
|
1187
1232
|
pointer-events: none; box-sizing: border-box;
|
|
1188
1233
|
}
|
|
1189
1234
|
.comty-inspector[data-selected] { pointer-events: auto; user-select: text; }
|
|
1190
|
-
.comty-inspector-head
|
|
1191
|
-
.comty-inspector-
|
|
1192
|
-
.comty-inspector-
|
|
1193
|
-
.comty-inspector-
|
|
1194
|
-
.comty-inspector-
|
|
1195
|
-
.comty-inspector
|
|
1196
|
-
.comty-inspector button
|
|
1235
|
+
.comty-inspector-head { display: flex; align-items: center; gap: .25rem; padding: 0 .125rem 0 .375rem; min-height: 1.75rem; }
|
|
1236
|
+
.comty-inspector-selector { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; color: var(--comty-muted); font-family: ui-monospace, monospace; }
|
|
1237
|
+
.comty-inspector-details { padding: .25rem .25rem .25rem .5rem; border-radius: .5rem; background: var(--comty-toolbar-surface); }
|
|
1238
|
+
.comty-inspector-row { display: grid; grid-template-columns: var(--comty-inspector-label-width) minmax(0, 1fr) var(--comty-copy-size); align-items: center; gap: .25rem; min-height: var(--comty-inspector-row-height); }
|
|
1239
|
+
.comty-inspector-label, .comty-inspector-secondary { color: var(--comty-muted); }
|
|
1240
|
+
.comty-inspector-label { overflow-wrap: anywhere; }
|
|
1241
|
+
.comty-inspector button { min-width: 0; padding: 0; border: 0; color: inherit; background: transparent; font: inherit; cursor: pointer; }
|
|
1197
1242
|
.comty-inspector button:focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .125rem; }
|
|
1198
|
-
.comty-inspector-value { display: flex;
|
|
1199
|
-
.comty-inspector
|
|
1200
|
-
.comty-inspector-
|
|
1201
|
-
.comty-inspector-
|
|
1202
|
-
.comty-inspector-
|
|
1203
|
-
.comty-inspector-
|
|
1204
|
-
.comty-inspector-
|
|
1205
|
-
.comty-inspector
|
|
1243
|
+
.comty-inspector-value { display: flex; align-items: center; gap: .3125rem; min-width: 0; padding-block: .125rem; text-align: left; font-variant-numeric: tabular-nums; }
|
|
1244
|
+
.comty-inspector button.comty-inspector-value { padding-block: .125rem; }
|
|
1245
|
+
.comty-inspector-text { min-width: 0; overflow-wrap: anywhere; }
|
|
1246
|
+
.comty-inspector-text:has(.comty-inspector-tokens) { display: flex; flex-wrap: wrap; gap: 0 .3125rem; }
|
|
1247
|
+
.comty-inspector-swatch { display: inline-block; flex: 0 0 .6875rem; width: .6875rem; height: .6875rem; border-radius: .1875rem; }
|
|
1248
|
+
.comty-inspector .comty-inspector-copy { display: grid; place-items: center; flex: 0 0 var(--comty-copy-size); width: var(--comty-copy-size); height: var(--comty-copy-size); border-radius: .375rem; }
|
|
1249
|
+
@media (hover: hover) and (pointer: fine) { .comty-inspector-copy:hover { background: var(--comty-hover-surface); } }
|
|
1250
|
+
@media (pointer: coarse) { .comty-inspector { --comty-copy-size: 2.75rem; } }
|
|
1206
1251
|
.comty-measure-line { position: fixed; background: var(--comty-outline); pointer-events: none; }
|
|
1207
1252
|
.comty-measure-tag { position: fixed; padding: .125rem .375rem; border-radius: .25rem; background: var(--comty-ink); color: var(--comty-paper); font: .625rem/1.4 ui-monospace, monospace; white-space: nowrap; pointer-events: none; }
|
|
1208
1253
|
|
|
@@ -1211,85 +1256,235 @@ function getComtyStyle() {
|
|
|
1211
1256
|
z-index: 2147483647;
|
|
1212
1257
|
background: var(--comty-surface);
|
|
1213
1258
|
border: 0;
|
|
1214
|
-
box-shadow:
|
|
1215
|
-
border-radius:
|
|
1216
|
-
width:
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
padding: 4px 4px 4px 16px;
|
|
1221
|
-
gap: 8px;
|
|
1259
|
+
box-shadow: var(--comty-floating-shadow);
|
|
1260
|
+
border-radius: 1.125rem;
|
|
1261
|
+
width: min(16rem, calc(100vw - 1rem));
|
|
1262
|
+
height: 2.25rem;
|
|
1263
|
+
overflow: hidden;
|
|
1264
|
+
font: .75rem/1.25rem var(--comty-font);
|
|
1222
1265
|
box-sizing: border-box;
|
|
1223
|
-
animation: comty-bubble-in
|
|
1266
|
+
animation: comty-bubble-in 200ms var(--comty-ease-out) forwards;
|
|
1224
1267
|
transform-origin: top left;
|
|
1268
|
+
transition: height var(--comty-motion-duration) var(--comty-ease-in-out), border-radius var(--comty-motion-duration) var(--comty-ease-in-out), box-shadow 160ms var(--comty-ease-out);
|
|
1225
1269
|
}
|
|
1226
1270
|
|
|
1227
|
-
.comty-create-bubble.comty-
|
|
1228
|
-
|
|
1229
|
-
|
|
1271
|
+
.comty-create-bubble.comty-create-expanded {
|
|
1272
|
+
height: 5.75rem;
|
|
1273
|
+
border-radius: .75rem;
|
|
1230
1274
|
}
|
|
1231
1275
|
|
|
1276
|
+
.comty-create-bubble.comty-create-has-attachment { height: 7.5rem; }
|
|
1277
|
+
.comty-create-bubble.comty-bubble-left { transform-origin: top right; }
|
|
1278
|
+
|
|
1232
1279
|
.comty-create-textarea {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1280
|
+
position: absolute;
|
|
1281
|
+
top: .5rem;
|
|
1282
|
+
left: .75rem;
|
|
1283
|
+
width: calc(100% - 3.25rem);
|
|
1284
|
+
height: 1.25rem;
|
|
1237
1285
|
border: none;
|
|
1238
1286
|
padding: 0;
|
|
1239
|
-
font
|
|
1240
|
-
font-size: 14px;
|
|
1241
|
-
line-height: 24px;
|
|
1287
|
+
font: inherit;
|
|
1242
1288
|
resize: none;
|
|
1243
1289
|
box-sizing: border-box;
|
|
1244
1290
|
outline: none;
|
|
1245
1291
|
background: transparent;
|
|
1246
|
-
overflow-y:
|
|
1292
|
+
overflow-y: hidden;
|
|
1247
1293
|
color: var(--comty-foreground);
|
|
1248
1294
|
scrollbar-width: thin;
|
|
1249
|
-
scrollbar-color:
|
|
1295
|
+
scrollbar-color: var(--comty-muted) transparent;
|
|
1296
|
+
transition: width var(--comty-motion-duration) var(--comty-ease-in-out), height var(--comty-motion-duration) var(--comty-ease-in-out);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
.comty-create-expanded .comty-create-textarea {
|
|
1300
|
+
width: calc(100% - 1.5rem);
|
|
1301
|
+
height: 2.5rem;
|
|
1302
|
+
overflow-y: auto;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
.comty-create-attachment {
|
|
1306
|
+
position: absolute;
|
|
1307
|
+
top: 3.25rem;
|
|
1308
|
+
left: .75rem;
|
|
1309
|
+
right: .75rem;
|
|
1310
|
+
display: none;
|
|
1311
|
+
min-width: 0;
|
|
1250
1312
|
}
|
|
1251
1313
|
|
|
1252
|
-
.comty-create-
|
|
1314
|
+
.comty-create-has-attachment .comty-create-attachment { display: flex; }
|
|
1315
|
+
.comty-create-attachment-chip {
|
|
1316
|
+
display: inline-flex;
|
|
1317
|
+
align-items: center;
|
|
1318
|
+
min-width: 0;
|
|
1319
|
+
max-width: 100%;
|
|
1320
|
+
padding: .125rem .125rem .125rem 0;
|
|
1321
|
+
border-radius: .375rem;
|
|
1322
|
+
background: var(--comty-field-surface);
|
|
1323
|
+
}
|
|
1324
|
+
.comty-create-attachment .comty-comment-link { flex: 0 1 auto; padding: 0 .5rem; line-height: 1.25rem; background: transparent; }
|
|
1325
|
+
.comty-create-attachment button {
|
|
1326
|
+
flex: 0 0 auto;
|
|
1327
|
+
width: 1.25rem;
|
|
1328
|
+
height: 1.25rem;
|
|
1329
|
+
padding: 0;
|
|
1330
|
+
border: 0;
|
|
1331
|
+
border-radius: .25rem;
|
|
1332
|
+
background: transparent;
|
|
1253
1333
|
color: var(--comty-muted);
|
|
1334
|
+
cursor: pointer;
|
|
1254
1335
|
}
|
|
1255
1336
|
|
|
1337
|
+
.comty-create-textarea::placeholder { color: var(--comty-muted); }
|
|
1338
|
+
|
|
1256
1339
|
.comty-create-actions {
|
|
1340
|
+
position: absolute;
|
|
1341
|
+
right: 0;
|
|
1342
|
+
bottom: 0;
|
|
1343
|
+
left: 0;
|
|
1257
1344
|
display: flex;
|
|
1258
1345
|
align-items: center;
|
|
1346
|
+
justify-content: space-between;
|
|
1347
|
+
height: 2.25rem;
|
|
1348
|
+
padding: .375rem .375rem .375rem .5rem;
|
|
1349
|
+
box-sizing: border-box;
|
|
1350
|
+
pointer-events: none;
|
|
1351
|
+
}
|
|
1352
|
+
.comty-create-actions::before {
|
|
1353
|
+
content: "";
|
|
1354
|
+
position: absolute;
|
|
1355
|
+
inset: 0 0 auto;
|
|
1356
|
+
height: .0625rem;
|
|
1357
|
+
background: var(--comty-border);
|
|
1358
|
+
opacity: 0;
|
|
1359
|
+
transition: opacity 110ms var(--comty-ease-out);
|
|
1360
|
+
}
|
|
1361
|
+
.comty-create-expanded .comty-create-actions::before { opacity: 1; transition-delay: 110ms; }
|
|
1362
|
+
.comty-create-actions > button { pointer-events: auto; }
|
|
1363
|
+
|
|
1364
|
+
.comty-create-link-button {
|
|
1365
|
+
display: grid;
|
|
1366
|
+
place-items: center;
|
|
1367
|
+
width: 1.5rem;
|
|
1368
|
+
height: 1.5rem;
|
|
1369
|
+
padding: 0;
|
|
1370
|
+
border: 0;
|
|
1371
|
+
border-radius: .375rem;
|
|
1372
|
+
background: transparent;
|
|
1373
|
+
color: var(--comty-muted);
|
|
1374
|
+
cursor: pointer;
|
|
1375
|
+
opacity: 0;
|
|
1376
|
+
visibility: hidden;
|
|
1377
|
+
transform: translateY(.25rem);
|
|
1378
|
+
transition: opacity 110ms var(--comty-ease-out), transform 110ms var(--comty-ease-out), visibility 0s linear 110ms;
|
|
1379
|
+
}
|
|
1380
|
+
.comty-create-expanded .comty-create-link-button {
|
|
1381
|
+
opacity: 1;
|
|
1382
|
+
visibility: visible;
|
|
1383
|
+
transform: none;
|
|
1384
|
+
transition-delay: 110ms, 110ms, 0s;
|
|
1385
|
+
}
|
|
1386
|
+
.comty-create-link-button svg { width: .75rem; height: .75rem; }
|
|
1387
|
+
|
|
1388
|
+
@media (hover: hover) and (pointer: fine) {
|
|
1389
|
+
.comty-create-link-button:hover,
|
|
1390
|
+
.comty-create-link-editor button:hover,
|
|
1391
|
+
.comty-create-attachment button:hover {
|
|
1392
|
+
background: var(--comty-hover-surface);
|
|
1393
|
+
color: var(--comty-foreground);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
.comty-create-link-button:focus-visible,
|
|
1397
|
+
.comty-create-link-editor button:focus-visible,
|
|
1398
|
+
.comty-create-attachment button:focus-visible,
|
|
1399
|
+
.comty-btn-submit:focus-visible {
|
|
1400
|
+
outline: .0625rem solid var(--comty-comment-focus);
|
|
1401
|
+
outline-offset: .0625rem;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
.comty-create-link-editor {
|
|
1405
|
+
position: absolute;
|
|
1406
|
+
inset: .375rem;
|
|
1407
|
+
display: flex;
|
|
1408
|
+
align-items: center;
|
|
1409
|
+
gap: .375rem;
|
|
1410
|
+
background: var(--comty-surface);
|
|
1411
|
+
opacity: 0;
|
|
1412
|
+
visibility: hidden;
|
|
1413
|
+
transform: translateY(.25rem);
|
|
1414
|
+
pointer-events: none;
|
|
1415
|
+
transition: opacity 160ms var(--comty-ease-out), transform 160ms var(--comty-ease-out), visibility 0s linear 160ms;
|
|
1416
|
+
}
|
|
1417
|
+
.comty-create-link-open .comty-create-link-editor {
|
|
1418
|
+
opacity: 1;
|
|
1419
|
+
visibility: visible;
|
|
1420
|
+
transform: none;
|
|
1421
|
+
pointer-events: auto;
|
|
1422
|
+
transition-delay: 0s;
|
|
1423
|
+
}
|
|
1424
|
+
.comty-create-link-field { position: relative; flex: 1; min-width: 0; }
|
|
1425
|
+
.comty-create-link-editor input {
|
|
1426
|
+
display: block;
|
|
1427
|
+
box-sizing: border-box;
|
|
1428
|
+
width: 100%;
|
|
1429
|
+
min-width: 0;
|
|
1430
|
+
height: 1.5rem;
|
|
1431
|
+
padding: 0 1.75rem 0 .5rem;
|
|
1432
|
+
border: 0;
|
|
1433
|
+
border-radius: .375rem;
|
|
1434
|
+
background: var(--comty-field-surface);
|
|
1435
|
+
color: var(--comty-foreground);
|
|
1436
|
+
font: inherit;
|
|
1437
|
+
outline: none;
|
|
1438
|
+
}
|
|
1439
|
+
.comty-create-link-editor input:focus-visible { background: var(--comty-hover-surface); }
|
|
1440
|
+
.comty-create-link-editor input[aria-invalid="true"] { box-shadow: inset 0 0 0 .0625rem var(--comty-error); }
|
|
1441
|
+
.comty-create-link-editor button {
|
|
1259
1442
|
flex: 0 0 auto;
|
|
1443
|
+
height: 1.5rem;
|
|
1444
|
+
padding: 0 .5rem;
|
|
1445
|
+
border: 0;
|
|
1446
|
+
border-radius: .375rem;
|
|
1447
|
+
background: var(--comty-field-surface);
|
|
1448
|
+
color: var(--comty-foreground);
|
|
1449
|
+
font: inherit;
|
|
1450
|
+
cursor: pointer;
|
|
1260
1451
|
}
|
|
1452
|
+
.comty-create-link-field button {
|
|
1453
|
+
position: absolute;
|
|
1454
|
+
top: .125rem;
|
|
1455
|
+
right: .125rem;
|
|
1456
|
+
width: 1.25rem;
|
|
1457
|
+
height: 1.25rem;
|
|
1458
|
+
padding: 0;
|
|
1459
|
+
background: transparent;
|
|
1460
|
+
}
|
|
1461
|
+
.comty-create-link-open .comty-create-link-button,
|
|
1462
|
+
.comty-create-link-open .comty-btn-submit { opacity: 0; visibility: hidden; pointer-events: none; }
|
|
1261
1463
|
|
|
1262
1464
|
.comty-btn-submit {
|
|
1263
1465
|
display: flex;
|
|
1264
1466
|
align-items: center;
|
|
1265
1467
|
justify-content: center;
|
|
1266
|
-
width:
|
|
1267
|
-
height:
|
|
1468
|
+
width: 1.5rem;
|
|
1469
|
+
height: 1.5rem;
|
|
1268
1470
|
padding: 0;
|
|
1269
1471
|
border-radius: 50%;
|
|
1270
1472
|
background: var(--comty-border);
|
|
1271
|
-
color:
|
|
1473
|
+
color: var(--comty-muted);
|
|
1272
1474
|
border: none;
|
|
1273
1475
|
cursor: pointer;
|
|
1274
|
-
transition: background
|
|
1476
|
+
transition: background-color 160ms var(--comty-ease-out), color 160ms var(--comty-ease-out), opacity 160ms var(--comty-ease-out);
|
|
1275
1477
|
}
|
|
1478
|
+
.comty-btn-submit svg { width: .875rem; height: .875rem; }
|
|
1479
|
+
.comty-btn-submit.comty-btn-active { background: var(--comty-foreground); color: var(--comty-surface); }
|
|
1480
|
+
.comty-btn-submit:disabled { opacity: .6; cursor: not-allowed; }
|
|
1276
1481
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
background: #0078FF;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
.comty-btn-submit.comty-btn-active:hover {
|
|
1287
|
-
background: #0062d1;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
.comty-btn-submit:disabled {
|
|
1291
|
-
opacity: 0.6;
|
|
1292
|
-
cursor: not-allowed;
|
|
1482
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1483
|
+
.comty-create-bubble, .comty-create-actions::before, .comty-create-textarea, .comty-create-link-button, .comty-create-link-editor, .comty-reply-field, .comty-reply-input {
|
|
1484
|
+
transition-duration: 0ms;
|
|
1485
|
+
transition-delay: 0ms;
|
|
1486
|
+
}
|
|
1487
|
+
.comty-create-bubble { animation: none; }
|
|
1293
1488
|
}
|
|
1294
1489
|
`;
|
|
1295
1490
|
}
|
|
@@ -1642,516 +1837,419 @@ function createAnchorMatcher({
|
|
|
1642
1837
|
.filter((candidate) => !isComtyNode(candidate));
|
|
1643
1838
|
const top = stack[0];
|
|
1644
1839
|
if (!top) return null;
|
|
1645
|
-
if (top === el || el.contains(top) || top.contains(el)) return null;
|
|
1646
|
-
return top;
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
function getVisibleAnchorPoint(el, relX, relY, activeModal) {
|
|
1650
|
-
if (!el || !Number.isFinite(relX) || !Number.isFinite(relY) || isElementTreeHidden(el)) return null;
|
|
1651
|
-
|
|
1652
|
-
const rect = el.getBoundingClientRect();
|
|
1653
|
-
if (rect.width < 0.1 || rect.height < 0.1) {
|
|
1654
|
-
return null;
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
const clientX = Math.min(
|
|
1658
|
-
Math.max(rect.left + relX * rect.width, rect.left + 0.01),
|
|
1659
|
-
rect.right - 0.01,
|
|
1660
|
-
);
|
|
1661
|
-
const clientY = Math.min(
|
|
1662
|
-
Math.max(rect.top + relY * rect.height, rect.top + 0.01),
|
|
1663
|
-
rect.bottom - 0.01,
|
|
1664
|
-
);
|
|
1665
|
-
const viewport = getViewportBounds();
|
|
1666
|
-
if (!pointIsInsideRect(clientX, clientY, viewport)) {
|
|
1667
|
-
return null;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
if (isPointClipped(el, clientX, clientY)) return null;
|
|
1671
|
-
|
|
1672
|
-
if (activeModal && !activeModal.contains(el)) {
|
|
1673
|
-
return null;
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
if (getOccludingElement(el, clientX, clientY)) return null;
|
|
1677
|
-
|
|
1678
|
-
return { clientX, clientY };
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
return { matchesAnchorContext, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
function createScreenshotCapture() {
|
|
1685
|
-
function nextAnimationFrame() {
|
|
1686
|
-
return new Promise((resolve) => {
|
|
1687
|
-
requestAnimationFrame(() => resolve());
|
|
1688
|
-
});
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
async function waitForPaint() {
|
|
1692
|
-
await nextAnimationFrame();
|
|
1693
|
-
await nextAnimationFrame();
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
async function waitForCustomElements(el, timeout = 1600) {
|
|
1697
|
-
if (
|
|
1698
|
-
!(el instanceof Element) ||
|
|
1699
|
-
!window.customElements ||
|
|
1700
|
-
typeof window.customElements.whenDefined !== "function"
|
|
1701
|
-
) {
|
|
1702
|
-
return;
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
const names = new Set();
|
|
1706
|
-
let current = el;
|
|
1707
|
-
while (current && current instanceof Element) {
|
|
1708
|
-
const tagName = current.tagName.toLowerCase();
|
|
1709
|
-
if (tagName.includes("-")) {
|
|
1710
|
-
names.add(tagName);
|
|
1711
|
-
}
|
|
1712
|
-
current = current.parentElement;
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
if (names.size === 0) return;
|
|
1716
|
-
|
|
1717
|
-
const definitions = [...names].map((name) =>
|
|
1718
|
-
window.customElements.whenDefined(name).catch(() => undefined),
|
|
1719
|
-
);
|
|
1720
|
-
await Promise.race([
|
|
1721
|
-
Promise.all(definitions),
|
|
1722
|
-
new Promise((resolve) => setTimeout(resolve, timeout)),
|
|
1723
|
-
]);
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
function syncFormValues(sourceRoot, cloneRoot) {
|
|
1727
|
-
const sourceFields = sourceRoot.querySelectorAll(
|
|
1728
|
-
"input, textarea, select",
|
|
1729
|
-
);
|
|
1730
|
-
const clonedFields = cloneRoot.querySelectorAll(
|
|
1731
|
-
"input, textarea, select",
|
|
1732
|
-
);
|
|
1733
|
-
|
|
1734
|
-
sourceFields.forEach((sourceField, index) => {
|
|
1735
|
-
const clonedField = clonedFields[index];
|
|
1736
|
-
if (!clonedField) return;
|
|
1737
|
-
|
|
1738
|
-
if (sourceField instanceof HTMLTextAreaElement) {
|
|
1739
|
-
clonedField.textContent = sourceField.value;
|
|
1740
|
-
clonedField.setAttribute("value", sourceField.value);
|
|
1741
|
-
return;
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
if (sourceField instanceof HTMLInputElement) {
|
|
1745
|
-
clonedField.setAttribute("value", sourceField.value);
|
|
1746
|
-
if (sourceField.checked) {
|
|
1747
|
-
clonedField.setAttribute("checked", "");
|
|
1748
|
-
} else {
|
|
1749
|
-
clonedField.removeAttribute("checked");
|
|
1750
|
-
}
|
|
1751
|
-
return;
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
if (sourceField instanceof HTMLSelectElement) {
|
|
1755
|
-
Array.from(sourceField.options).forEach((option, optionIndex) => {
|
|
1756
|
-
const clonedOption = clonedField.options[optionIndex];
|
|
1757
|
-
if (!clonedOption) return;
|
|
1758
|
-
if (option.selected) {
|
|
1759
|
-
clonedOption.setAttribute("selected", "");
|
|
1760
|
-
} else {
|
|
1761
|
-
clonedOption.removeAttribute("selected");
|
|
1762
|
-
}
|
|
1763
|
-
});
|
|
1764
|
-
}
|
|
1765
|
-
});
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
function toAbsoluteUrl(value) {
|
|
1769
|
-
if (!value || value.startsWith("data:") || value.startsWith("blob:")) {
|
|
1770
|
-
return value;
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
try {
|
|
1774
|
-
return new URL(value, window.location.href).toString();
|
|
1775
|
-
} catch {
|
|
1776
|
-
return value;
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
function isExportSafeUrl(value) {
|
|
1781
|
-
if (!value || value.startsWith("data:")) return true;
|
|
1782
|
-
|
|
1783
|
-
try {
|
|
1784
|
-
const url = new URL(value, window.location.href);
|
|
1785
|
-
return url.origin === window.location.origin;
|
|
1786
|
-
} catch {
|
|
1787
|
-
return false;
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
function blobToDataUrl(blob) {
|
|
1792
|
-
return new Promise((resolve, reject) => {
|
|
1793
|
-
const reader = new FileReader();
|
|
1794
|
-
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
1795
|
-
reader.onerror = reject;
|
|
1796
|
-
reader.readAsDataURL(blob);
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
async function urlToDataUrl(value) {
|
|
1801
|
-
const absoluteUrl = toAbsoluteUrl(value);
|
|
1802
|
-
if (!absoluteUrl || absoluteUrl.startsWith("data:")) return absoluteUrl;
|
|
1803
|
-
if (absoluteUrl.startsWith("blob:")) return "";
|
|
1804
|
-
|
|
1805
|
-
try {
|
|
1806
|
-
const response = await fetch(absoluteUrl, {
|
|
1807
|
-
credentials: isExportSafeUrl(absoluteUrl) ? "same-origin" : "omit",
|
|
1808
|
-
mode: "cors",
|
|
1809
|
-
});
|
|
1810
|
-
if (!response.ok) return "";
|
|
1811
|
-
return await blobToDataUrl(await response.blob());
|
|
1812
|
-
} catch {
|
|
1813
|
-
return "";
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
async function inlineImageResources(clone) {
|
|
1818
|
-
const images = Array.from(clone.querySelectorAll("img"));
|
|
1819
|
-
await Promise.all(
|
|
1820
|
-
images.map(async (image) => {
|
|
1821
|
-
const dataUrl = await urlToDataUrl(image.getAttribute("src") ?? "");
|
|
1822
|
-
if (!dataUrl) {
|
|
1823
|
-
image.removeAttribute("src");
|
|
1824
|
-
image.removeAttribute("srcset");
|
|
1825
|
-
image.style.visibility = "hidden";
|
|
1826
|
-
return;
|
|
1827
|
-
}
|
|
1828
|
-
image.setAttribute("src", dataUrl);
|
|
1829
|
-
image.removeAttribute("srcset");
|
|
1830
|
-
}),
|
|
1831
|
-
);
|
|
1832
|
-
|
|
1833
|
-
clone.querySelectorAll("source").forEach((source) => {
|
|
1834
|
-
source.removeAttribute("src");
|
|
1835
|
-
source.removeAttribute("srcset");
|
|
1836
|
-
});
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
function convertViewportUnits(value, viewportWidth, viewportHeight) {
|
|
1840
|
-
return value.replace(
|
|
1841
|
-
/(-?\d*\.?\d+)(dvh|svh|lvh|vh|dvw|svw|lvw|vw|vmin|vmax)\b/gi,
|
|
1842
|
-
(_, amount, unit) => {
|
|
1843
|
-
const number = Number(amount);
|
|
1844
|
-
if (!Number.isFinite(number)) return `${amount}${unit}`;
|
|
1845
|
-
const normalizedUnit = String(unit).toLowerCase();
|
|
1846
|
-
const base =
|
|
1847
|
-
normalizedUnit === "vw" ||
|
|
1848
|
-
normalizedUnit === "dvw" ||
|
|
1849
|
-
normalizedUnit === "svw" ||
|
|
1850
|
-
normalizedUnit === "lvw"
|
|
1851
|
-
? viewportWidth
|
|
1852
|
-
: normalizedUnit === "vmin"
|
|
1853
|
-
? Math.min(viewportWidth, viewportHeight)
|
|
1854
|
-
: normalizedUnit === "vmax"
|
|
1855
|
-
? Math.max(viewportWidth, viewportHeight)
|
|
1856
|
-
: viewportHeight;
|
|
1857
|
-
return `${(number / 100) * base}px`;
|
|
1858
|
-
},
|
|
1859
|
-
);
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
function sanitizeCssForExport(css, viewportWidth, viewportHeight) {
|
|
1863
|
-
return convertViewportUnits(css, viewportWidth, viewportHeight)
|
|
1864
|
-
.replace(/@font-face\s*{[^}]*}/gi, "")
|
|
1865
|
-
.replace(/url\((?!['"]?data:)[^)]+\)/gi, "none");
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
function sanitizeInlineStyles(clone, viewportWidth, viewportHeight) {
|
|
1869
|
-
clone.querySelectorAll("[style]").forEach((node) => {
|
|
1870
|
-
node.setAttribute(
|
|
1871
|
-
"style",
|
|
1872
|
-
sanitizeCssForExport(
|
|
1873
|
-
node.getAttribute("style") ?? "",
|
|
1874
|
-
viewportWidth,
|
|
1875
|
-
viewportHeight,
|
|
1876
|
-
),
|
|
1877
|
-
);
|
|
1878
|
-
});
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
function removeCaptureLockStyles(clone) {
|
|
1882
|
-
clone.querySelectorAll("[style]").forEach((node) => {
|
|
1883
|
-
const style = node.getAttribute("style") ?? "";
|
|
1884
|
-
const hasCaptureLock =
|
|
1885
|
-
/max-width:\s*none/i.test(style) &&
|
|
1886
|
-
/max-height:\s*none/i.test(style) &&
|
|
1887
|
-
/transform:\s*none/i.test(style) &&
|
|
1888
|
-
/margin:\s*0px/i.test(style);
|
|
1889
|
-
|
|
1890
|
-
if (!hasCaptureLock) return;
|
|
1891
|
-
|
|
1892
|
-
node.style.removeProperty("width");
|
|
1893
|
-
node.style.removeProperty("height");
|
|
1894
|
-
node.style.removeProperty("max-width");
|
|
1895
|
-
node.style.removeProperty("max-height");
|
|
1896
|
-
node.style.removeProperty("margin");
|
|
1897
|
-
node.style.removeProperty("transform");
|
|
1898
|
-
node.style.removeProperty("inset");
|
|
1899
|
-
});
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
function removeExportUnsafeNodes(clone) {
|
|
1903
|
-
clone
|
|
1904
|
-
.querySelectorAll("iframe, canvas, video, audio, object, embed")
|
|
1905
|
-
.forEach((node) => node.remove());
|
|
1906
|
-
clone
|
|
1907
|
-
.querySelectorAll("link[rel~='stylesheet'], link[rel~='preload']")
|
|
1908
|
-
.forEach((node) => node.remove());
|
|
1909
|
-
clone
|
|
1910
|
-
.querySelectorAll(
|
|
1911
|
-
"#comty-root, .comty-sidepanel, .comty-pin:not([data-comty-capture-keep]), .comty-bubble:not([data-comty-capture-keep]), .comty-create-bubble:not([data-comty-capture-keep]), .comty-popover",
|
|
1912
|
-
)
|
|
1913
|
-
.forEach((node) => node.remove());
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
function makeCloneUrlsAbsolute(clone) {
|
|
1917
|
-
clone.querySelectorAll("[src]").forEach((node) => {
|
|
1918
|
-
node.setAttribute("src", toAbsoluteUrl(node.getAttribute("src")));
|
|
1919
|
-
});
|
|
1920
|
-
clone.querySelectorAll("[href]").forEach((node) => {
|
|
1921
|
-
node.setAttribute("href", toAbsoluteUrl(node.getAttribute("href")));
|
|
1922
|
-
});
|
|
1923
|
-
clone.querySelectorAll("[srcset]").forEach((node) => {
|
|
1924
|
-
const srcset = node
|
|
1925
|
-
.getAttribute("srcset")
|
|
1926
|
-
.split(",")
|
|
1927
|
-
.map((entry) => {
|
|
1928
|
-
const parts = entry.trim().split(/\s+/);
|
|
1929
|
-
if (!parts[0]) return "";
|
|
1930
|
-
parts[0] = toAbsoluteUrl(parts[0]);
|
|
1931
|
-
return parts.join(" ");
|
|
1932
|
-
})
|
|
1933
|
-
.filter(Boolean)
|
|
1934
|
-
.join(", ");
|
|
1935
|
-
node.setAttribute("srcset", srcset);
|
|
1936
|
-
});
|
|
1840
|
+
if (top === el || el.contains(top) || top.contains(el)) return null;
|
|
1841
|
+
return top;
|
|
1937
1842
|
}
|
|
1938
1843
|
|
|
1939
|
-
function
|
|
1940
|
-
|
|
1941
|
-
let css = "";
|
|
1844
|
+
function getVisibleAnchorPoint(el, relX, relY, activeModal) {
|
|
1845
|
+
if (!el || !Number.isFinite(relX) || !Number.isFinite(relY) || isElementTreeHidden(el)) return null;
|
|
1942
1846
|
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
.join("\n");
|
|
1948
|
-
} catch {
|
|
1949
|
-
// Cross-origin stylesheets cannot be exported safely into canvas.
|
|
1950
|
-
}
|
|
1951
|
-
});
|
|
1847
|
+
const rect = el.getBoundingClientRect();
|
|
1848
|
+
if (rect.width < 0.1 || rect.height < 0.1) {
|
|
1849
|
+
return null;
|
|
1850
|
+
}
|
|
1952
1851
|
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
viewportWidth,
|
|
1957
|
-
viewportHeight,
|
|
1852
|
+
const clientX = Math.min(
|
|
1853
|
+
Math.max(rect.left + relX * rect.width, rect.left + 0.01),
|
|
1854
|
+
rect.right - 0.01,
|
|
1958
1855
|
);
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
function markCaptureElements(root, attributeName) {
|
|
1963
|
-
return [root, ...Array.from(root.querySelectorAll("*"))].map(
|
|
1964
|
-
(element, index) => {
|
|
1965
|
-
const previousValue = element.getAttribute(attributeName);
|
|
1966
|
-
element.setAttribute(attributeName, String(index));
|
|
1967
|
-
return { element, index, previousValue };
|
|
1968
|
-
},
|
|
1856
|
+
const clientY = Math.min(
|
|
1857
|
+
Math.max(rect.top + relY * rect.height, rect.top + 0.01),
|
|
1858
|
+
rect.bottom - 0.01,
|
|
1969
1859
|
);
|
|
1970
|
-
|
|
1860
|
+
const viewport = getViewportBounds();
|
|
1861
|
+
if (!pointIsInsideRect(clientX, clientY, viewport)) {
|
|
1862
|
+
return null;
|
|
1863
|
+
}
|
|
1971
1864
|
|
|
1972
|
-
|
|
1973
|
-
markedElements.forEach(({ element, previousValue }) => {
|
|
1974
|
-
if (previousValue === null) {
|
|
1975
|
-
element.removeAttribute(attributeName);
|
|
1976
|
-
return;
|
|
1977
|
-
}
|
|
1978
|
-
element.setAttribute(attributeName, previousValue);
|
|
1979
|
-
});
|
|
1980
|
-
}
|
|
1865
|
+
if (isPointClipped(el, clientX, clientY)) return null;
|
|
1981
1866
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
.querySelectorAll(`[${attributeName}]`)
|
|
1986
|
-
.forEach((element) => element.removeAttribute(attributeName));
|
|
1987
|
-
}
|
|
1867
|
+
if (activeModal && !activeModal.contains(el)) {
|
|
1868
|
+
return null;
|
|
1869
|
+
}
|
|
1988
1870
|
|
|
1989
|
-
|
|
1990
|
-
clone,
|
|
1991
|
-
markedElements,
|
|
1992
|
-
attributeName,
|
|
1993
|
-
scrollX,
|
|
1994
|
-
scrollY,
|
|
1995
|
-
viewportWidth,
|
|
1996
|
-
viewportHeight,
|
|
1997
|
-
) {
|
|
1998
|
-
markedElements.forEach(({ element, index }) => {
|
|
1999
|
-
if (window.getComputedStyle(element).position !== "fixed") return;
|
|
1871
|
+
if (getOccludingElement(el, clientX, clientY)) return null;
|
|
2000
1872
|
|
|
2001
|
-
|
|
2002
|
-
`[${attributeName}="${index}"]`,
|
|
2003
|
-
);
|
|
2004
|
-
if (!(clonedElement instanceof HTMLElement)) return;
|
|
2005
|
-
|
|
2006
|
-
const rect = element.getBoundingClientRect();
|
|
2007
|
-
clonedElement.style.position = "absolute";
|
|
2008
|
-
clonedElement.style.left = `${rect.left + scrollX}px`;
|
|
2009
|
-
clonedElement.style.top = `${rect.top + scrollY}px`;
|
|
2010
|
-
clonedElement.style.right = "auto";
|
|
2011
|
-
clonedElement.style.bottom = "auto";
|
|
2012
|
-
clonedElement.style.boxSizing = "border-box";
|
|
2013
|
-
clonedElement.style.width = `${rect.width || viewportWidth}px`;
|
|
2014
|
-
clonedElement.style.height = `${rect.height || viewportHeight}px`;
|
|
2015
|
-
clonedElement.style.transform = "none";
|
|
2016
|
-
});
|
|
1873
|
+
return { clientX, clientY };
|
|
2017
1874
|
}
|
|
2018
1875
|
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
markedElements,
|
|
2022
|
-
attributeName,
|
|
2023
|
-
) {
|
|
2024
|
-
markedElements.forEach(({ element, index }) => {
|
|
2025
|
-
if (element === document.documentElement || element === document.body) {
|
|
2026
|
-
return;
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
const scrollLeft = element.scrollLeft;
|
|
2030
|
-
const scrollTop = element.scrollTop;
|
|
2031
|
-
if (!scrollLeft && !scrollTop) return;
|
|
2032
|
-
|
|
2033
|
-
const clonedElement = clone.querySelector(
|
|
2034
|
-
`[${attributeName}="${index}"]`,
|
|
2035
|
-
);
|
|
2036
|
-
if (!(clonedElement instanceof HTMLElement)) return;
|
|
2037
|
-
|
|
2038
|
-
const children = Array.from(clonedElement.childNodes);
|
|
2039
|
-
if (!children.length) return;
|
|
2040
|
-
|
|
2041
|
-
const scrollWrapper = clone.ownerDocument.createElement("div");
|
|
2042
|
-
const computedStyle = window.getComputedStyle(element);
|
|
2043
|
-
scrollWrapper.style.transform = `translate(${-scrollLeft}px, ${-scrollTop}px)`;
|
|
2044
|
-
scrollWrapper.style.transformOrigin = "top left";
|
|
2045
|
-
scrollWrapper.style.width = `${Math.max(
|
|
2046
|
-
element.scrollWidth,
|
|
2047
|
-
element.clientWidth,
|
|
2048
|
-
)}px`;
|
|
2049
|
-
scrollWrapper.style.minHeight = `${Math.max(
|
|
2050
|
-
element.scrollHeight,
|
|
2051
|
-
element.clientHeight,
|
|
2052
|
-
)}px`;
|
|
1876
|
+
return { matchesAnchorContext, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
|
|
1877
|
+
}
|
|
2053
1878
|
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
1879
|
+
const captureEngine = (() => { // Generated from html-to-image 1.11.13 by scripts/capture-engine.mjs.
|
|
1880
|
+
/*
|
|
1881
|
+
MIT License
|
|
1882
|
+
|
|
1883
|
+
Copyright (c) 2017-2025 W.Y.
|
|
1884
|
+
|
|
1885
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
1886
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
1887
|
+
in the Software without restriction, including without limitation the rights
|
|
1888
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
1889
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
1890
|
+
furnished to do so, subject to the following conditions:
|
|
1891
|
+
|
|
1892
|
+
The above copyright notice and this permission notice shall be included in all
|
|
1893
|
+
copies or substantial portions of the Software.
|
|
1894
|
+
|
|
1895
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
1896
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
1897
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
1898
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
1899
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
1900
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
1901
|
+
SOFTWARE.
|
|
1902
|
+
|
|
1903
|
+
*/
|
|
1904
|
+
function S(t,e){if(t.match(/^[a-z]+:\/\//i))return t;if(t.match(/^\/\//))return window.location.protocol+t;if(t.match(/^[a-z]+:/i))return t;let r=document.implementation.createHTMLDocument(),n=r.createElement("base"),i=r.createElement("a");return r.head.appendChild(n),r.body.appendChild(i),e&&(n.href=e),i.href=t,i.href}var P=(()=>{let t=0,e=()=>`0000${(Math.random()*36**4<<0).toString(36)}`.slice(-4);return()=>(t+=1,`u${e()}${t}`)})();function u(t){let e=[];for(let r=0,n=t.length;r<n;r++)e.push(t[r]);return e}var f=null;function y(t={}){return f||(t.includeStyleProperties?(f=t.includeStyleProperties,f):(f=u(window.getComputedStyle(document.documentElement)),f))}function w(t){return new Promise((e,r)=>{let n=new Image;n.onload=()=>{n.decode().then(()=>{requestAnimationFrame(()=>e(n))})},n.onerror=r,n.crossOrigin="anonymous",n.decoding="async",n.src=t})}async function D(t){return Promise.resolve().then(()=>new XMLSerializer().serializeToString(t)).then(encodeURIComponent).then(e=>`data:image/svg+xml;charset=utf-8,${e}`)}async function I(t,e,r){let n="http://www.w3.org/2000/svg",i=document.createElementNS(n,"svg"),o=document.createElementNS(n,"foreignObject");return i.setAttribute("width",`${e}`),i.setAttribute("height",`${r}`),i.setAttribute("viewBox",`0 0 ${e} ${r}`),o.setAttribute("width","100%"),o.setAttribute("height","100%"),o.setAttribute("x","0"),o.setAttribute("y","0"),o.setAttribute("externalResourcesRequired","true"),i.appendChild(o),o.appendChild(t),D(i)}var l=(t,e)=>{if(t instanceof e)return!0;let r=Object.getPrototypeOf(t);return r===null?!1:r.constructor.name===e.name||l(r,e)};function U(t){let e=t.getPropertyValue("content");return`${t.cssText} content: '${e.replace(/'|"/g,"")}';`}function V(t,e){return y(e).map(r=>{let n=t.getPropertyValue(r),i=t.getPropertyPriority(r);return`${r}: ${n}${i?" !important":""};`}).join(" ")}function M(t,e,r,n){let i=`.${t}:${e}`,o=r.cssText?U(r):V(r,n);return document.createTextNode(`${i}{${o}}`)}function R(t,e,r,n){let i=window.getComputedStyle(t,r),o=i.getPropertyValue("content");if(o===""||o==="none")return;let a=P();try{e.className=`${e.className} ${a}`}catch{return}let c=document.createElement("style");c.appendChild(M(a,r,i,n)),e.appendChild(c)}function C(t,e,r){R(t,e,":before",r),R(t,e,":after",r)}var T="application/font-woff",$="image/jpeg",F={woff:T,woff2:T,ttf:"application/font-truetype",eot:"application/vnd.ms-fontobject",png:"image/png",jpg:$,jpeg:$,gif:"image/gif",tiff:"image/tiff",svg:"image/svg+xml",webp:"image/webp"};function H(t){let e=/\.([^./]*?)$/g.exec(t);return e?e[1]:""}function m(t){let e=H(t).toLowerCase();return F[e]||""}function O(t){return t.split(/,/)[1]}function p(t){return t.search(/^(data:)/)!==-1}function x(t,e){return`data:${e};base64,${t}`}async function k(t,e,r){let n=await(e?.captureFetch||fetch)(t,e);if(!n.ok)throw new Error(`Resource "${n.url}" not found`);let i=await n.blob();return new Promise((o,a)=>{let c=new FileReader;c.onerror=a,c.onloadend=()=>{try{o(r({res:n,result:c.result}))}catch(s){a(s)}},c.readAsDataURL(i)})}async function h(t,e,r){let n=r.resourceCache||{},i=t;if(n[i]!=null)return n[i];r.cacheBust&&(t+=(/\?/.test(t)?"&":"?")+new Date().getTime());let o;try{let a=await k(t,r.fetchRequestInit,({res:c,result:s})=>(e||(e=c.headers.get("Content-Type")||""),O(s)));o=x(a,e)}catch(a){o=r.imagePlaceholder||"";let c=`Failed to fetch resource: ${t}`;a&&(c=typeof a=="string"?a:a.message),c&&console.warn(c)}return n[i]=o,o}async function q(t){let e=t.toDataURL();return e==="data:,"?t.cloneNode(!1):w(e)}async function _(t,e){if(t.currentSrc){let o=document.createElement("canvas"),a=o.getContext("2d");o.width=t.clientWidth,o.height=t.clientHeight,a?.drawImage(t,0,0,o.width,o.height);let c=o.toDataURL();return w(c)}let r=t.poster,n=m(r),i=await h(r,n,e);return w(i)}async function v(t,e){var r;try{if(!((r=t?.contentDocument)===null||r===void 0)&&r.body)return await b(t.contentDocument.body,e,!0)}catch{}return t.cloneNode(!1)}async function B(t,e){return l(t,HTMLCanvasElement)?q(t).catch(()=>t.cloneNode(!1)):l(t,HTMLVideoElement)?_(t,e).catch(()=>document.createElement("div")):l(t,HTMLIFrameElement)?v(t,e):t.cloneNode(!1)}var G=t=>t.tagName!=null&&t.tagName.toUpperCase()==="SLOT";async function j(t,e,r){var n,i;let o=[];return G(t)&&t.assignedNodes?o=u(t.assignedNodes()):l(t,HTMLIFrameElement)&&(!((n=t.contentDocument)===null||n===void 0)&&n.body)?o=u(t.contentDocument.body.childNodes):o=u(((i=t.shadowRoot)!==null&&i!==void 0?i:t).childNodes),o.length===0||l(t,HTMLVideoElement)||await o.reduce((a,c)=>a.then(()=>b(c,r)).then(s=>{s&&e.appendChild(s)}),Promise.resolve()),e}function z(t,e,r){let n=e.style;if(!n)return;let i=window.getComputedStyle(t);i.cssText?(n.cssText=i.cssText,n.transformOrigin=i.transformOrigin):y(r).forEach(o=>{let a=i.getPropertyValue(o);o==="font-size"&&a.endsWith("px")&&(a=`${parseFloat(a.substring(0,a.length-2))}px`),l(t,HTMLIFrameElement)&&o==="display"&&a==="inline"&&(a="block"),o==="d"&&e.getAttribute("d")&&(a=`path(${e.getAttribute("d")})`),n.setProperty(o,a,i.getPropertyPriority(o))})}function W(t,e){l(t,HTMLTextAreaElement)&&(e.textContent=t.value),l(t,HTMLInputElement)&&e.setAttribute("value",t.value)}function X(t,e){if(l(t,HTMLSelectElement)){let n=Array.from(e.children).find(i=>t.value===i.getAttribute("value"));n&&n.setAttribute("selected","")}}function K(t,e,r){return l(e,Element)&&(z(t,e,r),C(t,e,r),W(t,e),X(t,e)),e}async function J(t,e){let r=t.querySelectorAll?t.querySelectorAll("use"):[];if(r.length===0)return t;let n={};for(let o=0;o<r.length;o++){let a=r[o],c=a.getAttribute("href")||a.getAttribute("xlink:href");if(c){let s=t.querySelector(c),d=document.querySelector(c);!s&&d&&!n[c]&&(n[c]=await b(d,e,!0))}}let i=Object.values(n);if(i.length){let o="http://www.w3.org/1999/xhtml",a=document.createElementNS(o,"svg");a.setAttribute("xmlns",o),a.style.position="absolute",a.style.width="0",a.style.height="0",a.style.overflow="hidden",a.style.display="none";let c=document.createElementNS(o,"defs");a.appendChild(c);for(let s=0;s<i.length;s++)c.appendChild(i[s]);t.appendChild(a)}return t}async function b(t,e,r){if(await e.beforeClone?.(t),!r&&e.filter&&!e.filter(t))return null;let n=e.placeholder?.(t);return n||Promise.resolve(t).then(i=>B(i,e)).then(i=>j(t,i,e)).then(async i=>(await e.beforeClone?.(t),K(t,i,e))).then(i=>e.afterClone?.(t,i)??i).then(i=>J(i,e))}var L=/url\((['"]?)([^'"]+?)\1\)/g,Q=/url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g,Y=/src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g;function Z(t){let e=t.replace(/([.*+?^${}()|\[\]\/\\])/g,"\\$1");return new RegExp(`(url\\(['"]?)(${e})(['"]?\\))`,"g")}function N(t){let e=[];return t.replace(L,(r,n,i)=>(e.push(i),r)),e.filter(r=>!p(r)&&!r.startsWith("#"))}async function tt(t,e,r,n,i){try{let o=r?S(e,r):e,a=m(e),c;if(i){let s=await i(o);c=x(s,a)}else c=await h(o,a,n);return t.replace(Z(e),`$1${c}$3`)}catch{}return t}function et(t,{preferredFontFormat:e}){return e?t.replace(Y,r=>{for(;;){let[n,,i]=Q.exec(r)||[];if(!i)return"";if(i===e)return`src: ${n};`}}):t}function rt(t){return t.search(L)!==-1}async function E(t,e,r){if(!rt(t))return t;let n=et(t,r);return N(n).reduce((o,a)=>o.then(c=>tt(c,a,e,r)),Promise.resolve(n))}async function g(t,e,r){var n;let i=(n=e.style)===null||n===void 0?void 0:n.getPropertyValue(t);if(i){let o=await E(i,null,r);return e.style.setProperty(t,o,e.style.getPropertyPriority(t)),!0}return!1}async function nt(t,e){await g("background",t,e)||await g("background-image",t,e),await g("mask",t,e)||await g("-webkit-mask",t,e)||await g("mask-image",t,e)||await g("-webkit-mask-image",t,e)}async function it(t,e){let r=l(t,HTMLImageElement);if(!(r&&!p(t.src))&&!(l(t,SVGImageElement)&&!p(t.href.baseVal)))return;let n=r?t.src:t.href.baseVal,i=await h(n,m(n),e);await new Promise((o,a)=>{t.onload=o,t.onerror=e.onImageErrorHandler?(...s)=>{try{o(e.onImageErrorHandler(...s))}catch(d){a(d)}}:a;let c=t;c.decode&&(c.decode=o),c.loading==="lazy"&&(c.loading="eager"),r?(t.srcset="",t.src=i):t.href.baseVal=i})}async function ot(t,e){let n=u(t.childNodes).map(i=>A(i,e));await Promise.all(n).then(()=>t)}async function A(t,e){await e.beforeClone?.(t),l(t,Element)&&(await nt(t,e),await it(t,e),await ot(t,e))} return {cloneNode:b,embedImages:A,embedResources:E,nodeToDataURL:I}; })();
|
|
1905
|
+
// Fetches belong to one capture. Never forward SDK credentials to a resource host.
|
|
1906
|
+
function createCaptureResources({ apiUrl, projectKey, getHeaders, signal }) {
|
|
1907
|
+
const cache = new Map();
|
|
1908
|
+
const waiting = [];
|
|
1909
|
+
let active = 0;
|
|
1910
|
+
let totalBytes = 0;
|
|
1911
|
+
async function fetchResource(value) {
|
|
1912
|
+
const url = new URL(value, document.baseURI);
|
|
1913
|
+
if (!["http:", "https:", "blob:", "data:"].includes(url.protocol)) throw new Error("Unsupported capture resource");
|
|
1914
|
+
if (cache.has(url.href)) return (await cache.get(url.href)).clone();
|
|
1915
|
+
if (cache.size >= 128) throw new Error("Too many capture resources");
|
|
1916
|
+
const pending = (async () => {
|
|
1917
|
+
if (active >= 4) await new Promise(resolve => waiting.push(resolve));
|
|
1918
|
+
active++;
|
|
1919
|
+
try {
|
|
1920
|
+
signal.throwIfAborted();
|
|
1921
|
+
let response;
|
|
1922
|
+
try {
|
|
1923
|
+
response = await fetch(url, { signal: AbortSignal.any([signal, AbortSignal.timeout(3500)]), credentials: url.origin === location.origin ? "same-origin" : "omit" });
|
|
1924
|
+
if (!response.ok) throw new Error("Resource unavailable");
|
|
1925
|
+
} catch (error) {
|
|
1926
|
+
if (signal.aborted || !apiUrl || !["http:", "https:"].includes(url.protocol) || url.origin === location.origin) throw error;
|
|
1927
|
+
const proxy = new URL(`${apiUrl}/api/sdk/capture-resource`);
|
|
1928
|
+
proxy.searchParams.set("projectKey", projectKey);
|
|
1929
|
+
proxy.searchParams.set("url", url.href);
|
|
1930
|
+
response = await fetch(proxy, { signal, headers: getHeaders(), credentials: "omit" });
|
|
1931
|
+
if (!response.ok) throw new Error("Capture proxy unavailable");
|
|
2067
1932
|
}
|
|
1933
|
+
const reader = response.body.getReader();
|
|
1934
|
+
const chunks = [];
|
|
1935
|
+
let size = 0;
|
|
1936
|
+
try {
|
|
1937
|
+
for (;;) {
|
|
1938
|
+
const { done, value } = await reader.read();
|
|
1939
|
+
if (done) break;
|
|
1940
|
+
size += value.byteLength;
|
|
1941
|
+
totalBytes += value.byteLength;
|
|
1942
|
+
if (size > 5 * 1024 * 1024 || totalBytes > 24 * 1024 * 1024) throw new Error("Capture resources too large");
|
|
1943
|
+
chunks.push(value);
|
|
1944
|
+
}
|
|
1945
|
+
} finally { await reader.cancel().catch(() => {}); }
|
|
1946
|
+
const blob = new Blob(chunks, { type: response.headers.get("Content-Type") || "application/octet-stream" });
|
|
1947
|
+
return new Response(blob, { headers: { "Content-Type": blob.type } });
|
|
1948
|
+
} finally {
|
|
1949
|
+
active--;
|
|
1950
|
+
waiting.shift()?.();
|
|
1951
|
+
}
|
|
1952
|
+
})();
|
|
1953
|
+
cache.set(url.href, pending);
|
|
1954
|
+
return (await pending).clone();
|
|
1955
|
+
}
|
|
1956
|
+
return fetchResource;
|
|
1957
|
+
}
|
|
2068
1958
|
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
1959
|
+
// Read font rules without inserting remote CSS into the user's live stylesheets.
|
|
1960
|
+
async function captureFontCSS(sheets, families, fetchResource, embedResources, options) {
|
|
1961
|
+
const visited = new Set();
|
|
1962
|
+
const fonts = [];
|
|
1963
|
+
const normalize = value => value.trim().replace(/^['"]|['"]$/g, "").toLowerCase();
|
|
1964
|
+
async function readRules(rules, base) {
|
|
1965
|
+
for (const rule of rules) {
|
|
1966
|
+
if (rule.type === CSSRule.FONT_FACE_RULE && families.has(normalize(rule.style.getPropertyValue("font-family")))) {
|
|
1967
|
+
const woff2 = /format\(["']?woff2["']?\)/i.test(rule.style.getPropertyValue("src"));
|
|
1968
|
+
fonts.push(embedResources(rule.cssText, base, woff2 ? { ...options, preferredFontFormat: "woff2" } : options));
|
|
1969
|
+
} else if (rule.type === CSSRule.IMPORT_RULE) {
|
|
1970
|
+
await readSheet(rule.styleSheet, new URL(rule.href, base).href);
|
|
1971
|
+
} else if (rule.cssRules) await readRules(rule.cssRules, base);
|
|
2072
1972
|
}
|
|
1973
|
+
}
|
|
1974
|
+
async function readSheet(sheet, url = sheet?.href) {
|
|
1975
|
+
const key = url || sheet;
|
|
1976
|
+
if (!key || visited.has(key)) return;
|
|
1977
|
+
visited.add(key);
|
|
1978
|
+
try {
|
|
1979
|
+
let rules;
|
|
1980
|
+
try { rules = sheet.cssRules; } catch { /* Remote CSS requires CORS or the resource proxy. */ }
|
|
1981
|
+
if (rules) return await readRules(rules, url || document.baseURI);
|
|
1982
|
+
if (!url) return;
|
|
1983
|
+
const css = await (await fetchResource(url)).text();
|
|
1984
|
+
// Constructed sheets parse nested font rules but intentionally discard @import.
|
|
1985
|
+
// Resolve imports separately, relative to the stylesheet rather than the page.
|
|
1986
|
+
const imports = [...css.replace(/\/\*[\s\S]*?\*\//g, "").matchAll(/@import\s+(?:url\(\s*(?:"([^"]+)"|'([^']+)'|([^\s)]+))\s*\)|"([^"]+)"|'([^']+)')/gi)];
|
|
1987
|
+
await Promise.all(imports.map(match => readSheet(null, new URL(match.slice(1).find(Boolean), url).href)));
|
|
1988
|
+
const parsed = new CSSStyleSheet();
|
|
1989
|
+
parsed.replaceSync(css);
|
|
1990
|
+
await readRules(parsed.cssRules, url);
|
|
1991
|
+
} catch (error) {
|
|
1992
|
+
if (options.signal.aborted) throw error;
|
|
1993
|
+
console.warn("Comty: a capture font stylesheet was unavailable", url);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
await Promise.all([...sheets].map(sheet => readSheet(sheet)));
|
|
1997
|
+
return (await Promise.all(fonts)).join("\n");
|
|
1998
|
+
}
|
|
2073
1999
|
|
|
2074
|
-
async function captureViewportScreenshot() {
|
|
2075
|
-
await waitForPaint();
|
|
2076
2000
|
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
const scrollY = window.scrollY;
|
|
2081
|
-
const captureWidth = Math.max(width + scrollX, width);
|
|
2082
|
-
const documentHeight = Math.max(
|
|
2083
|
-
document.documentElement.scrollHeight,
|
|
2084
|
-
document.body?.scrollHeight ?? 0,
|
|
2085
|
-
height,
|
|
2086
|
-
);
|
|
2087
|
-
const captureIdAttribute = "data-comty-capture-id";
|
|
2088
|
-
const markedElements = markCaptureElements(
|
|
2089
|
-
document.documentElement,
|
|
2090
|
-
captureIdAttribute,
|
|
2091
|
-
);
|
|
2092
|
-
let clone;
|
|
2001
|
+
function captureContext() {
|
|
2002
|
+
return { url: location.href, width: innerWidth, height: innerHeight, scrollX, scrollY };
|
|
2003
|
+
}
|
|
2093
2004
|
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
markedElements,
|
|
2099
|
-
captureIdAttribute,
|
|
2100
|
-
scrollX,
|
|
2101
|
-
scrollY,
|
|
2102
|
-
width,
|
|
2103
|
-
height,
|
|
2104
|
-
);
|
|
2105
|
-
syncScrollableElementOffsets(clone, markedElements, captureIdAttribute);
|
|
2106
|
-
} finally {
|
|
2107
|
-
restoreCaptureElementMarks(markedElements, captureIdAttribute);
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
clone.querySelectorAll("script").forEach((node) => node.remove());
|
|
2111
|
-
syncFormValues(document, clone);
|
|
2112
|
-
makeCloneUrlsAbsolute(clone);
|
|
2113
|
-
await inlineImageResources(clone);
|
|
2114
|
-
inlineAccessibleStyles(clone, width, height);
|
|
2115
|
-
sanitizeInlineStyles(clone, width, height);
|
|
2116
|
-
removeCaptureLockStyles(clone);
|
|
2117
|
-
removeExportUnsafeNodes(clone);
|
|
2118
|
-
removeCaptureElementMarks(clone, captureIdAttribute);
|
|
2119
|
-
clone.style.width = `${captureWidth}px`;
|
|
2120
|
-
clone.style.height = `${documentHeight}px`;
|
|
2121
|
-
clone.style.margin = "0";
|
|
2122
|
-
clone.style.overflow = "visible";
|
|
2005
|
+
async function yieldCapture() {
|
|
2006
|
+
if (globalThis.scheduler?.yield) await globalThis.scheduler.yield();
|
|
2007
|
+
else await new Promise(resolve => setTimeout(resolve, 0));
|
|
2008
|
+
}
|
|
2123
2009
|
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2010
|
+
const captureExcluded = "#comty-root, .comty-sidepanel, .comty-pin, .comty-bubble, .comty-create-bubble, .comty-popover, .comty-temp-pin, [data-comty-capture-exclude]";
|
|
2011
|
+
const transparentPixel = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
|
|
2012
|
+
|
|
2013
|
+
function createScreenshotCapture({ apiUrl, projectKey, getHeaders = () => ({}) } = {}) {
|
|
2014
|
+
async function bounded(promise, ms) {
|
|
2015
|
+
let timer;
|
|
2016
|
+
try { return await Promise.race([promise, new Promise(resolve => { timer = setTimeout(resolve, ms); })]); }
|
|
2017
|
+
finally { clearTimeout(timer); }
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
async function captureViewportScreenshot({ context = captureContext(), annotation } = {}) {
|
|
2021
|
+
if (document.visibilityState === "hidden") return null;
|
|
2022
|
+
const controller = new AbortController();
|
|
2023
|
+
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
2024
|
+
const { signal } = controller;
|
|
2025
|
+
const untilAborted = promise => new Promise((resolve, reject) => {
|
|
2026
|
+
const aborted = () => reject(new Error("Screenshot timed out"));
|
|
2027
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
2028
|
+
Promise.resolve(promise).then(resolve, reject).finally(() => signal.removeEventListener("abort", aborted));
|
|
2029
|
+
if (signal.aborted) aborted();
|
|
2030
|
+
});
|
|
2031
|
+
let frozen = false;
|
|
2032
|
+
let lastYield = performance.now();
|
|
2033
|
+
const checkContext = () => {
|
|
2034
|
+
signal.throwIfAborted();
|
|
2035
|
+
if (!frozen && (context.url !== location.href || context.width !== innerWidth || context.height !== innerHeight || context.scrollX !== scrollX || context.scrollY !== scrollY)) {
|
|
2036
|
+
throw new Error("Page moved before its screenshot was ready");
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
const cooperate = async () => {
|
|
2040
|
+
checkContext();
|
|
2041
|
+
if (performance.now() - lastYield >= 6) {
|
|
2042
|
+
await yieldCapture();
|
|
2043
|
+
lastYield = performance.now();
|
|
2044
|
+
checkContext();
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
try {
|
|
2048
|
+
await yieldCapture();
|
|
2049
|
+
await bounded(document.fonts?.ready || Promise.resolve(), 1200);
|
|
2050
|
+
checkContext();
|
|
2051
|
+
const resources = createCaptureResources({ apiUrl, projectKey, getHeaders, signal });
|
|
2052
|
+
const fontFamilies = new Set();
|
|
2053
|
+
const sheets = new Set([...document.styleSheets, ...document.adoptedStyleSheets]);
|
|
2054
|
+
const addFonts = value => value.split(",").forEach(family => fontFamilies.add(family.trim().replace(/^['"]|['"]$/g, "").toLowerCase()));
|
|
2055
|
+
const positioned = [];
|
|
2056
|
+
let cloningAnnotation = false;
|
|
2057
|
+
const options = {
|
|
2058
|
+
resourceCache: Object.create(null),
|
|
2059
|
+
includeQueryParams: true,
|
|
2060
|
+
imagePlaceholder: transparentPixel,
|
|
2061
|
+
signal,
|
|
2062
|
+
fetchRequestInit: { captureFetch: resources },
|
|
2063
|
+
beforeClone: cooperate,
|
|
2064
|
+
placeholder(source) {
|
|
2065
|
+
if (cloningAnnotation) return null;
|
|
2066
|
+
// Offscreen leaf boxes still occupy their exact layout space, but need no
|
|
2067
|
+
// text, fonts or hundreds of computed declarations in the exported SVG.
|
|
2068
|
+
if (!(source instanceof HTMLElement)) return null;
|
|
2069
|
+
if (/^(IFRAME|OBJECT|EMBED)$/.test(source.tagName)) {
|
|
2070
|
+
const spacer = document.createElement("div");
|
|
2071
|
+
const style = getComputedStyle(source);
|
|
2072
|
+
for (const property of style) spacer.style.setProperty(property, style.getPropertyValue(property));
|
|
2073
|
+
return spacer;
|
|
2074
|
+
}
|
|
2075
|
+
if (source.children.length || source.shadowRoot) return null;
|
|
2076
|
+
const rect = source.getBoundingClientRect();
|
|
2077
|
+
if (!rect.width || !rect.height || (rect.bottom > 0 && rect.top < context.height && rect.right > 0 && rect.left < context.width)) return null;
|
|
2078
|
+
const style = getComputedStyle(source);
|
|
2079
|
+
if (!["static", "relative"].includes(style.position) || !["block", "flex", "grid"].includes(style.display) || style.transform !== "none") return null;
|
|
2080
|
+
const spacer = document.createElement("div");
|
|
2081
|
+
for (const property of ["display", "margin", "flex", "align-self", "grid-area", "order"]) spacer.style.setProperty(property, style.getPropertyValue(property));
|
|
2082
|
+
Object.assign(spacer.style, { width: `${rect.width}px`, height: `${rect.height}px`, boxSizing: "border-box", visibility: "hidden" });
|
|
2083
|
+
return spacer;
|
|
2084
|
+
},
|
|
2085
|
+
filter(node) {
|
|
2086
|
+
if (!(node instanceof Element)) return true;
|
|
2087
|
+
if ((!cloningAnnotation && node.matches(captureExcluded)) || /^(SCRIPT|STYLE|LINK|HEAD|NOSCRIPT|AUDIO)$/.test(node.tagName)) return false;
|
|
2088
|
+
return getComputedStyle(node).display !== "none";
|
|
2089
|
+
},
|
|
2090
|
+
afterClone(source, clone) {
|
|
2091
|
+
if (!(source instanceof Element) || !clone.style) return clone;
|
|
2092
|
+
const style = getComputedStyle(source);
|
|
2093
|
+
addFonts(style.fontFamily);
|
|
2094
|
+
for (const pseudo of ["::before", "::after"]) {
|
|
2095
|
+
const pseudoStyle = getComputedStyle(source, pseudo);
|
|
2096
|
+
if (pseudoStyle.content && !["none", "normal"].includes(pseudoStyle.content)) addFonts(pseudoStyle.fontFamily);
|
|
2097
|
+
}
|
|
2098
|
+
if (source.shadowRoot) {
|
|
2099
|
+
for (const sheet of [...source.shadowRoot.styleSheets, ...source.shadowRoot.adoptedStyleSheets]) sheets.add(sheet);
|
|
2100
|
+
}
|
|
2101
|
+
clone.style.setProperty("animation", "none", "important");
|
|
2102
|
+
clone.style.setProperty("transition", "none", "important");
|
|
2103
|
+
clone.style.setProperty("caret-color", "transparent", "important");
|
|
2104
|
+
// Chromium 140 exposes these values but omits them from style enumeration.
|
|
2105
|
+
for (const property of ["text-underline-offset", "text-decoration-thickness"]) {
|
|
2106
|
+
clone.style.setProperty(property, style.getPropertyValue(property));
|
|
2107
|
+
}
|
|
2108
|
+
// WebKit scrollbar pseudo-styles are not part of the computed element style.
|
|
2109
|
+
if ([style.overflowX, style.overflowY].some(value => value === "auto" || value === "scroll") && getComputedStyle(source, "::-webkit-scrollbar").display === "none") {
|
|
2110
|
+
clone.style.setProperty("scrollbar-width", "none", "important");
|
|
2111
|
+
}
|
|
2112
|
+
// A cloned document must not re-run handlers or select a different responsive image.
|
|
2113
|
+
for (const attribute of [...clone.attributes]) {
|
|
2114
|
+
if (attribute.name.startsWith("on")) clone.removeAttribute(attribute.name);
|
|
2115
|
+
}
|
|
2116
|
+
if (source instanceof HTMLImageElement) {
|
|
2117
|
+
clone.removeAttribute("srcset");
|
|
2118
|
+
clone.removeAttribute("sizes");
|
|
2119
|
+
clone.src = source.currentSrc || source.src || transparentPixel;
|
|
2120
|
+
clone.loading = "eager";
|
|
2121
|
+
}
|
|
2122
|
+
if (source instanceof HTMLSourceElement) {
|
|
2123
|
+
clone.removeAttribute("srcset");
|
|
2124
|
+
clone.removeAttribute("src");
|
|
2125
|
+
}
|
|
2126
|
+
if (source instanceof HTMLInputElement) {
|
|
2127
|
+
if (source.type === "password") clone.setAttribute("value", "•".repeat(source.value.length));
|
|
2128
|
+
clone.toggleAttribute("checked", source.checked);
|
|
2129
|
+
}
|
|
2130
|
+
if (source instanceof HTMLSelectElement) {
|
|
2131
|
+
[...clone.options].forEach((option, index) => option.toggleAttribute("selected", source.options[index]?.selected));
|
|
2132
|
+
}
|
|
2133
|
+
const rect = source.getBoundingClientRect();
|
|
2134
|
+
if (!cloningAnnotation && ["fixed", "sticky"].includes(style.position)) {
|
|
2135
|
+
// Composite viewport-bound elements independently of the document's scroll.
|
|
2136
|
+
// Keep a spacer for sticky flow; the real source is never moved or restyled.
|
|
2137
|
+
positioned.push({ clone, rect, zIndex: Number.parseInt(style.zIndex, 10) || 0 });
|
|
2138
|
+
const spacer = document.createElement("div");
|
|
2139
|
+
if (style.position === "sticky") {
|
|
2140
|
+
spacer.style.cssText = clone.style.cssText;
|
|
2141
|
+
spacer.style.position = "static";
|
|
2142
|
+
spacer.style.visibility = "hidden";
|
|
2143
|
+
spacer.style.width = `${source.offsetWidth}px`;
|
|
2144
|
+
spacer.style.height = `${source.offsetHeight}px`;
|
|
2145
|
+
} else spacer.style.display = "none";
|
|
2146
|
+
return spacer;
|
|
2147
|
+
}
|
|
2148
|
+
if (source !== document.documentElement && source !== document.body && (source.scrollTop || source.scrollLeft)) {
|
|
2149
|
+
const wrapper = document.createElement("div");
|
|
2150
|
+
for (const property of ["display", "flex-direction", "flex-wrap", "align-items", "justify-content", "gap", "grid-template-columns", "grid-template-rows"]) {
|
|
2151
|
+
wrapper.style.setProperty(property, style.getPropertyValue(property));
|
|
2152
|
+
}
|
|
2153
|
+
const horizontalPadding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
|
2154
|
+
wrapper.style.width = `${Math.max(source.scrollWidth, source.clientWidth) - horizontalPadding}px`;
|
|
2155
|
+
wrapper.style.transform = `translate(${-source.scrollLeft}px, ${-source.scrollTop}px)`;
|
|
2156
|
+
wrapper.style.transformOrigin = "top left";
|
|
2157
|
+
wrapper.append(...clone.childNodes);
|
|
2158
|
+
clone.append(wrapper);
|
|
2159
|
+
clone.style.display = "block";
|
|
2160
|
+
}
|
|
2161
|
+
return clone;
|
|
2162
|
+
},
|
|
2151
2163
|
};
|
|
2164
|
+
const clone = await captureEngine.cloneNode(document.documentElement, options, true);
|
|
2165
|
+
checkContext();
|
|
2166
|
+
frozen = true;
|
|
2167
|
+
const stage = document.createElement("div");
|
|
2168
|
+
stage.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
2169
|
+
stage.style.position = "relative";
|
|
2170
|
+
stage.style.width = `${context.width}px`;
|
|
2171
|
+
stage.style.height = `${context.height}px`;
|
|
2172
|
+
stage.style.overflow = "hidden";
|
|
2173
|
+
// The browser propagates the root (or body's) background to the viewport;
|
|
2174
|
+
// an HTML clone inside SVG does not, especially with collapsed margins.
|
|
2175
|
+
const rootStyle = getComputedStyle(document.documentElement);
|
|
2176
|
+
stage.style.backgroundColor = rootStyle.backgroundColor === "rgba(0, 0, 0, 0)" && rootStyle.backgroundImage === "none"
|
|
2177
|
+
? getComputedStyle(document.body).backgroundColor : rootStyle.backgroundColor;
|
|
2178
|
+
const documentLayer = document.createElement("div");
|
|
2179
|
+
documentLayer.style.transform = `translate(${-context.scrollX}px, ${-context.scrollY}px)`;
|
|
2180
|
+
documentLayer.style.transformOrigin = "top left";
|
|
2181
|
+
clone.style.width = `${context.width}px`;
|
|
2182
|
+
clone.style.height = `${Math.max(document.documentElement.scrollHeight, context.height)}px`;
|
|
2183
|
+
clone.style.overflow = "visible";
|
|
2184
|
+
clone.style.margin = "0";
|
|
2185
|
+
documentLayer.append(clone);
|
|
2186
|
+
stage.append(documentLayer);
|
|
2187
|
+
for (const item of positioned.sort((a, b) => a.zIndex - b.zIndex)) {
|
|
2188
|
+
const { clone: node, rect } = item;
|
|
2189
|
+
if (rect.bottom <= 0 || rect.right <= 0 || rect.top >= context.height || rect.left >= context.width) continue;
|
|
2190
|
+
Object.assign(node.style, { position: "absolute", left: `${rect.left}px`, top: `${rect.top}px`, right: "auto", bottom: "auto", width: `${rect.width}px`, height: `${rect.height}px`, boxSizing: "border-box", margin: "0", transform: "none" });
|
|
2191
|
+
stage.append(node);
|
|
2192
|
+
}
|
|
2193
|
+
if (annotation) {
|
|
2194
|
+
// Measure the real Comty components in an invisible host, then include
|
|
2195
|
+
// their styled clones in the same font/image embedding as the page.
|
|
2196
|
+
const mounted = annotation();
|
|
2197
|
+
try {
|
|
2198
|
+
cloningAnnotation = true;
|
|
2199
|
+
for (const source of mounted.elements) {
|
|
2200
|
+
const rect = source.getBoundingClientRect();
|
|
2201
|
+
const node = await captureEngine.cloneNode(source, options, true);
|
|
2202
|
+
Object.assign(node.style, { position: "absolute", left: `${rect.left}px`, top: `${rect.top}px`, right: "auto", bottom: "auto", width: `${rect.width}px`, height: `${rect.height}px`, boxSizing: "border-box", margin: "0", transform: "none" });
|
|
2203
|
+
stage.append(node);
|
|
2204
|
+
}
|
|
2205
|
+
} finally {
|
|
2206
|
+
cloningAnnotation = false;
|
|
2207
|
+
mounted.dispose();
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
const fonts = await untilAborted(captureFontCSS(sheets, fontFamilies, resources, captureEngine.embedResources, options));
|
|
2211
|
+
if (fonts) {
|
|
2212
|
+
const style = document.createElement("style");
|
|
2213
|
+
style.textContent = fonts;
|
|
2214
|
+
stage.prepend(style);
|
|
2215
|
+
}
|
|
2216
|
+
// Pseudo-element styles are generated by the library and need their URLs embedded too.
|
|
2217
|
+
for (const style of stage.querySelectorAll("style")) {
|
|
2218
|
+
await cooperate();
|
|
2219
|
+
style.textContent = await captureEngine.embedResources(style.textContent, document.baseURI, options);
|
|
2220
|
+
}
|
|
2221
|
+
await untilAborted(captureEngine.embedImages(stage, options));
|
|
2222
|
+
checkContext();
|
|
2223
|
+
await yieldCapture();
|
|
2224
|
+
const svg = await captureEngine.nodeToDataURL(stage, context.width, context.height);
|
|
2225
|
+
const image = new Image();
|
|
2226
|
+
image.src = svg;
|
|
2227
|
+
await untilAborted(image.decode());
|
|
2228
|
+
checkContext();
|
|
2229
|
+
const scale = Math.min(2, devicePixelRatio || 1, 2560 / context.width, 2560 / context.height);
|
|
2230
|
+
const canvas = document.createElement("canvas");
|
|
2231
|
+
canvas.width = Math.round(context.width * scale);
|
|
2232
|
+
canvas.height = Math.round(context.height * scale);
|
|
2233
|
+
const ctx = canvas.getContext("2d");
|
|
2234
|
+
ctx.scale(scale, scale);
|
|
2235
|
+
ctx.drawImage(image, 0, 0);
|
|
2236
|
+
let blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/png")));
|
|
2237
|
+
if (!blob) throw new Error("Screenshot encoding failed");
|
|
2238
|
+
if (blob.size > 2800000) {
|
|
2239
|
+
ctx.globalCompositeOperation = "destination-over";
|
|
2240
|
+
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue("background-color");
|
|
2241
|
+
ctx.fillRect(0, 0, context.width, context.height);
|
|
2242
|
+
blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", 0.9)));
|
|
2243
|
+
if (!blob) throw new Error("Screenshot encoding failed");
|
|
2244
|
+
}
|
|
2245
|
+
// CSS dimensions keep the eyedropper's coordinate system independent of DPR.
|
|
2246
|
+
return { blob, width: canvas.width, height: canvas.height, cssWidth: context.width, cssHeight: context.height };
|
|
2247
|
+
} finally {
|
|
2248
|
+
clearTimeout(timeout);
|
|
2249
|
+
controller.abort();
|
|
2152
2250
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2251
|
+
}
|
|
2252
|
+
return { captureViewportScreenshot };
|
|
2155
2253
|
}
|
|
2156
2254
|
|
|
2157
2255
|
// Page-local counterpart of the native eyedropper; never captures outside the page.
|
|
@@ -2199,7 +2297,9 @@ function createColorMagnifier({ root, isComtyElement, capture }) {
|
|
|
2199
2297
|
sampledHex = null;
|
|
2200
2298
|
if (!enabled) return;
|
|
2201
2299
|
try {
|
|
2202
|
-
const
|
|
2300
|
+
const screenshot = await capture();
|
|
2301
|
+
if (!screenshot) return;
|
|
2302
|
+
const { blob, cssWidth, cssHeight } = screenshot;
|
|
2203
2303
|
if (!enabled || version !== revision) return;
|
|
2204
2304
|
const dataUrl = await new Promise((resolve, reject) => {
|
|
2205
2305
|
const reader = new FileReader();
|
|
@@ -2215,7 +2315,11 @@ function createColorMagnifier({ root, isComtyElement, capture }) {
|
|
|
2215
2315
|
const probe = document.createElement('canvas').getContext('2d');
|
|
2216
2316
|
probe.drawImage(next, 0, 0);
|
|
2217
2317
|
probe.getImageData(0, 0, 1, 1);
|
|
2218
|
-
|
|
2318
|
+
const scaled = document.createElement('canvas');
|
|
2319
|
+
scaled.width = cssWidth || next.width;
|
|
2320
|
+
scaled.height = cssHeight || next.height;
|
|
2321
|
+
scaled.getContext('2d').drawImage(next, 0, 0, scaled.width, scaled.height);
|
|
2322
|
+
image = scaled;
|
|
2219
2323
|
paint();
|
|
2220
2324
|
} catch { /* The CSS-colour fallback remains available without screen permission. */ }
|
|
2221
2325
|
}
|
|
@@ -2262,7 +2366,7 @@ function createCommentThread(context) {
|
|
|
2262
2366
|
const {
|
|
2263
2367
|
root, apiUrl, projectKey, commentStatuses, selectedStatusFilters, placedPins,
|
|
2264
2368
|
closeBubbles, getCommentStatus, updateCommentStatus, applyStatusDot,
|
|
2265
|
-
renderSidepanelComments, applyStatusFilters, createThreadItem,
|
|
2369
|
+
renderSidepanelComments, applyStatusFilters, createThreadItem, renderCommentContent,
|
|
2266
2370
|
isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
|
|
2267
2371
|
updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
|
|
2268
2372
|
showAuthPrompt,
|
|
@@ -2290,14 +2394,6 @@ function createCommentThread(context) {
|
|
|
2290
2394
|
const header = document.createElement("div");
|
|
2291
2395
|
header.className = "comty-bubble-header";
|
|
2292
2396
|
|
|
2293
|
-
const title = document.createElement("h2");
|
|
2294
|
-
title.className = "comty-bubble-title";
|
|
2295
|
-
title.innerText = "Comment";
|
|
2296
|
-
header.appendChild(title);
|
|
2297
|
-
|
|
2298
|
-
const headerActions = document.createElement("div");
|
|
2299
|
-
headerActions.className = "comty-bubble-header-actions";
|
|
2300
|
-
|
|
2301
2397
|
const statusMenuButton = document.createElement("button");
|
|
2302
2398
|
statusMenuButton.className = "comty-status-trigger";
|
|
2303
2399
|
statusMenuButton.type = "button";
|
|
@@ -2336,7 +2432,7 @@ function createCommentThread(context) {
|
|
|
2336
2432
|
};
|
|
2337
2433
|
|
|
2338
2434
|
const saveStatus = async (nextStatus, control) => {
|
|
2339
|
-
if (!context.
|
|
2435
|
+
if (!context.canManage) return false;
|
|
2340
2436
|
control.disabled = true;
|
|
2341
2437
|
const savedStatus = await updateCommentStatus(comment, nextStatus);
|
|
2342
2438
|
control.disabled = false;
|
|
@@ -2361,7 +2457,7 @@ function createCommentThread(context) {
|
|
|
2361
2457
|
return true;
|
|
2362
2458
|
};
|
|
2363
2459
|
|
|
2364
|
-
statusMenuButton.disabled = !context.
|
|
2460
|
+
statusMenuButton.disabled = !context.canManage;
|
|
2365
2461
|
statusMenuButton.onclick = (event) => {
|
|
2366
2462
|
event.stopPropagation();
|
|
2367
2463
|
const existingPopover = document.querySelector(
|
|
@@ -2373,10 +2469,11 @@ function createCommentThread(context) {
|
|
|
2373
2469
|
if (existingPopover) return;
|
|
2374
2470
|
|
|
2375
2471
|
const popover = document.createElement("div");
|
|
2376
|
-
popover.className = "comty-popover comty-status-popover";
|
|
2472
|
+
popover.className = "comty-popover comty-comment-popover comty-status-popover";
|
|
2377
2473
|
const buttonRect = statusMenuButton.getBoundingClientRect();
|
|
2378
2474
|
popover.style.top = `${buttonRect.bottom + 4}px`;
|
|
2379
|
-
popover.style.left = `${buttonRect.
|
|
2475
|
+
popover.style.left = `${buttonRect.left}px`;
|
|
2476
|
+
popover.style.transformOrigin = "top left";
|
|
2380
2477
|
|
|
2381
2478
|
for (const status of commentStatuses) {
|
|
2382
2479
|
const item = document.createElement("button");
|
|
@@ -2414,6 +2511,13 @@ function createCommentThread(context) {
|
|
|
2414
2511
|
}
|
|
2415
2512
|
|
|
2416
2513
|
root.appendChild(popover);
|
|
2514
|
+
const popoverWidth = popover.offsetWidth;
|
|
2515
|
+
const popoverHeight = popover.offsetHeight;
|
|
2516
|
+
popover.style.left = `${Math.max(8, Math.min(buttonRect.left, innerWidth - popoverWidth - 8))}px`;
|
|
2517
|
+
if (buttonRect.bottom + 4 + popoverHeight > innerHeight - 8) {
|
|
2518
|
+
popover.style.top = `${Math.max(8, buttonRect.top - popoverHeight - 4)}px`;
|
|
2519
|
+
popover.style.transformOrigin = "bottom left";
|
|
2520
|
+
}
|
|
2417
2521
|
|
|
2418
2522
|
const closeStatusPopover = (closeEvent) => {
|
|
2419
2523
|
if (
|
|
@@ -2430,9 +2534,7 @@ function createCommentThread(context) {
|
|
|
2430
2534
|
);
|
|
2431
2535
|
};
|
|
2432
2536
|
|
|
2433
|
-
|
|
2434
|
-
headerActions.appendChild(closeButton);
|
|
2435
|
-
header.appendChild(headerActions);
|
|
2537
|
+
header.append(statusMenuButton, closeButton);
|
|
2436
2538
|
bubble.appendChild(header);
|
|
2437
2539
|
|
|
2438
2540
|
const thread = document.createElement("div");
|
|
@@ -2451,7 +2553,7 @@ function createCommentThread(context) {
|
|
|
2451
2553
|
|
|
2452
2554
|
const popover = document.createElement("div");
|
|
2453
2555
|
popover.className =
|
|
2454
|
-
"comty-popover comty-entry-actions-popover";
|
|
2556
|
+
"comty-popover comty-comment-popover comty-entry-actions-popover";
|
|
2455
2557
|
|
|
2456
2558
|
const editItem = document.createElement("button");
|
|
2457
2559
|
editItem.className = "comty-popover-item";
|
|
@@ -2537,7 +2639,7 @@ function createCommentThread(context) {
|
|
|
2537
2639
|
|
|
2538
2640
|
const previousContent = entry.content;
|
|
2539
2641
|
entry.content = nextContent;
|
|
2540
|
-
item.content
|
|
2642
|
+
renderCommentContent(item.content, nextContent);
|
|
2541
2643
|
editor.disabled = true;
|
|
2542
2644
|
editor.replaceWith(item.content);
|
|
2543
2645
|
renderSidepanelComments();
|
|
@@ -2545,11 +2647,11 @@ function createCommentThread(context) {
|
|
|
2545
2647
|
const savedContent = await persist(nextContent);
|
|
2546
2648
|
if (!savedContent) {
|
|
2547
2649
|
entry.content = previousContent;
|
|
2548
|
-
item.content
|
|
2650
|
+
renderCommentContent(item.content, previousContent || "");
|
|
2549
2651
|
renderSidepanelComments();
|
|
2550
2652
|
return;
|
|
2551
2653
|
}
|
|
2552
|
-
item.content
|
|
2654
|
+
renderCommentContent(item.content, savedContent);
|
|
2553
2655
|
renderSidepanelComments();
|
|
2554
2656
|
};
|
|
2555
2657
|
|
|
@@ -2655,9 +2757,11 @@ function createCommentThread(context) {
|
|
|
2655
2757
|
replyForm.appendChild(createCommentAvatar(context.currentUser || { name: "You" }));
|
|
2656
2758
|
const replyField = document.createElement("div");
|
|
2657
2759
|
replyField.className = "comty-reply-field";
|
|
2658
|
-
const replyInput = document.createElement("
|
|
2760
|
+
const replyInput = document.createElement("textarea");
|
|
2659
2761
|
replyInput.className = "comty-reply-input";
|
|
2660
2762
|
replyInput.placeholder = "Reply";
|
|
2763
|
+
replyInput.rows = 1;
|
|
2764
|
+
replyInput.setAttribute("aria-label", "Reply");
|
|
2661
2765
|
|
|
2662
2766
|
const replyButton = document.createElement("button");
|
|
2663
2767
|
replyButton.className = "comty-reply-button";
|
|
@@ -2691,6 +2795,7 @@ function createCommentThread(context) {
|
|
|
2691
2795
|
comment.replies = replies;
|
|
2692
2796
|
appendReply(data.reply);
|
|
2693
2797
|
replyInput.value = "";
|
|
2798
|
+
syncReplySize();
|
|
2694
2799
|
requestAnimationFrame(() => {
|
|
2695
2800
|
threadList.scrollTop = threadList.scrollHeight;
|
|
2696
2801
|
});
|
|
@@ -2704,12 +2809,28 @@ function createCommentThread(context) {
|
|
|
2704
2809
|
}
|
|
2705
2810
|
};
|
|
2706
2811
|
|
|
2812
|
+
const syncReplySize = () => {
|
|
2813
|
+
const currentHeight = replyInput.getBoundingClientRect().height;
|
|
2814
|
+
replyInput.style.transition = "none";
|
|
2815
|
+
const lineHeight = parseFloat(getComputedStyle(replyInput).lineHeight);
|
|
2816
|
+
replyInput.style.height = `${lineHeight}px`;
|
|
2817
|
+
const contentHeight = replyInput.scrollHeight;
|
|
2818
|
+
replyInput.style.height = `${currentHeight}px`;
|
|
2819
|
+
replyInput.getBoundingClientRect();
|
|
2820
|
+
replyInput.style.transition = "";
|
|
2821
|
+
const nextHeight = Math.min(contentHeight, parseFloat(getComputedStyle(replyInput).maxHeight));
|
|
2822
|
+
replyInput.style.height = `${nextHeight}px`;
|
|
2823
|
+
if (contentHeight <= lineHeight + 1) replyInput.scrollTop = 0;
|
|
2824
|
+
replyField.classList.toggle("comty-reply-expanded", contentHeight > lineHeight + 1);
|
|
2825
|
+
};
|
|
2826
|
+
|
|
2707
2827
|
replyInput.oninput = () => {
|
|
2828
|
+
syncReplySize();
|
|
2708
2829
|
replyButton.disabled = replyInput.value.trim().length === 0;
|
|
2709
2830
|
};
|
|
2710
2831
|
replyInput.onkeydown = (event) => {
|
|
2711
2832
|
event.stopPropagation();
|
|
2712
|
-
if (event.key === "Enter" && !event.shiftKey) {
|
|
2833
|
+
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
|
|
2713
2834
|
event.preventDefault();
|
|
2714
2835
|
void submitReply();
|
|
2715
2836
|
}
|
|
@@ -2727,10 +2848,10 @@ function createCommentThread(context) {
|
|
|
2727
2848
|
const signInButton = document.createElement("button");
|
|
2728
2849
|
signInButton.className = "comty-sign-in-reply";
|
|
2729
2850
|
signInButton.type = "button";
|
|
2730
|
-
signInButton.innerText = "Sign in to reply";
|
|
2851
|
+
signInButton.innerText = context.allowGuestComments ? "Leave a reply" : "Sign in to reply";
|
|
2731
2852
|
signInButton.onclick = (event) => {
|
|
2732
2853
|
event.stopPropagation();
|
|
2733
|
-
showAuthPrompt();
|
|
2854
|
+
showAuthPrompt(false, () => openCommentThread(comment, pin, pinStatusDot, x, y, removeHoverBubble));
|
|
2734
2855
|
};
|
|
2735
2856
|
replyForm.appendChild(signInButton);
|
|
2736
2857
|
}
|
|
@@ -3020,11 +3141,14 @@ function initComty(options = {}) {
|
|
|
3020
3141
|
const observedCommentTargets = new WeakSet();
|
|
3021
3142
|
let sidepanelOpen = false;
|
|
3022
3143
|
let canWrite = false;
|
|
3144
|
+
let canManage = false;
|
|
3145
|
+
let allowGuestComments = false;
|
|
3023
3146
|
let loginUrl = "";
|
|
3024
3147
|
let currentUser = null;
|
|
3025
3148
|
let authStatus = "";
|
|
3026
3149
|
let didCompleteSignIn = false;
|
|
3027
3150
|
const tokenStorageKey = `comty:${projectKey}:token`;
|
|
3151
|
+
const authReturnStorageKey = `comty:${projectKey}:auth-return`;
|
|
3028
3152
|
let sdkToken = "";
|
|
3029
3153
|
const activeHoverCommentIds = new Set();
|
|
3030
3154
|
/** @type {null | 'edit' | 'browse'} */
|
|
@@ -3033,7 +3157,6 @@ function initComty(options = {}) {
|
|
|
3033
3157
|
const placementDiagnostics = new Map();
|
|
3034
3158
|
let inspectedElement = null;
|
|
3035
3159
|
let selectedElement = null;
|
|
3036
|
-
let inspectExpanded = false;
|
|
3037
3160
|
let measureAlt = false;
|
|
3038
3161
|
let inspectDrag = null;
|
|
3039
3162
|
let measuredRegion = null;
|
|
@@ -3715,6 +3838,21 @@ function initComty(options = {}) {
|
|
|
3715
3838
|
const copyIcon = icon('<rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/>');
|
|
3716
3839
|
const checkIcon = icon('<path d="m5 12 4 4 10-10"/>');
|
|
3717
3840
|
const copyFeedbackTimers = new WeakMap();
|
|
3841
|
+
async function copyPropertyValue(button, feedback) {
|
|
3842
|
+
clearTimeout(copyFeedbackTimers.get(button));
|
|
3843
|
+
try {
|
|
3844
|
+
await navigator.clipboard.writeText(button.dataset.copy);
|
|
3845
|
+
if (!button.isConnected) return;
|
|
3846
|
+
button.dataset.copied = "";
|
|
3847
|
+
feedback.textContent = "Copied";
|
|
3848
|
+
clearTimeout(copyFeedbackTimers.get(button));
|
|
3849
|
+
copyFeedbackTimers.set(button, setTimeout(() => { delete button.dataset.copied; }, 1200));
|
|
3850
|
+
} catch {
|
|
3851
|
+
delete button.dataset.copied;
|
|
3852
|
+
if (button.isConnected) feedback.textContent = "Clipboard unavailable — select the value to copy";
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
|
|
3718
3856
|
function propertyRow(label, value, display = value) {
|
|
3719
3857
|
const unavailable = value === "—";
|
|
3720
3858
|
return `<div class="comty-property-row"><span>${label}</span><button type="button" data-copy="${escapeInspectorText(value)}" aria-label="Copy ${label}: ${escapeInspectorText(display)}" ${unavailable ? "disabled" : ""}><span>${escapeInspectorText(display)}</span>${unavailable ? "" : `<i class="comty-property-copy-icon" aria-hidden="true">${copyIcon}${checkIcon}</i>`}</button></div>`;
|
|
@@ -3741,7 +3879,7 @@ function initComty(options = {}) {
|
|
|
3741
3879
|
propertyPanel.setAttribute("aria-label", propertyKind === "color" ? "Recent colours" : "Recent typography");
|
|
3742
3880
|
const recent = propertyKind === "color" ? recentColors : recentFonts;
|
|
3743
3881
|
const details = propertyKind === "color" && expanded
|
|
3744
|
-
? `<div class="comty-property-detail"><div class="comty-property-rows">${propertyRow("HEX", propertySelection.hex)}${propertyRow("RGB", propertySelection.rgb)}${propertyRow("HSL", propertySelection.hsl)}${propertyRow("OKLCH", propertySelection.oklch)}${propertyRow("Token", propertySelection.token ?
|
|
3882
|
+
? `<div class="comty-property-detail"><div class="comty-property-rows">${propertyRow("HEX", propertySelection.hex)}${propertyRow("RGB", propertySelection.rgb)}${propertyRow("HSL", propertySelection.hsl)}${propertyRow("OKLCH", propertySelection.oklch)}${propertyRow("Token", propertySelection.token ? propertySelection.token.replace(/^--/, "") : "—", propertySelection.token || "—")}</div></div>`
|
|
3745
3883
|
: propertyKind === "font" && expanded
|
|
3746
3884
|
? `<div class="comty-property-detail"><div class="comty-property-rows">${propertyRow("Family", propertySelection.family)}${propertyRow("Size", propertySelection.size)}${propertyRow("Weight", propertySelection.weight)}${propertyRow("Leading", propertySelection.leading)}${propertyRow("Tracking", propertySelection.tracking)}${propertyRow("Colour", propertySelection.color?.hex || "—")}${propertyRow("CSS", propertySelection.css, "All of the above")}</div></div>`
|
|
3747
3885
|
: "";
|
|
@@ -3857,18 +3995,7 @@ function initComty(options = {}) {
|
|
|
3857
3995
|
renderPropertyPanel(true);
|
|
3858
3996
|
propertyPanel.querySelector(`button[data-recent="${button.dataset.recent}"]`)?.focus({ preventScroll: true });
|
|
3859
3997
|
} else if (button.dataset.copy != null && button.dataset.copy !== "—") {
|
|
3860
|
-
|
|
3861
|
-
try {
|
|
3862
|
-
await navigator.clipboard.writeText(button.dataset.copy);
|
|
3863
|
-
if (!button.isConnected || propertyPanel.hidden) return;
|
|
3864
|
-
button.dataset.copied = "";
|
|
3865
|
-
propertyPanel.querySelector(".comty-property-feedback").textContent = `${button.getAttribute("aria-label").slice(5)} copied`;
|
|
3866
|
-
clearTimeout(copyFeedbackTimers.get(button));
|
|
3867
|
-
copyFeedbackTimers.set(button, setTimeout(() => { delete button.dataset.copied; }, 1200));
|
|
3868
|
-
} catch {
|
|
3869
|
-
delete button.dataset.copied;
|
|
3870
|
-
if (button.isConnected && !propertyPanel.hidden) propertyPanel.querySelector(".comty-property-feedback").textContent = "Clipboard unavailable";
|
|
3871
|
-
}
|
|
3998
|
+
await copyPropertyValue(button, propertyPanel.querySelector(".comty-property-feedback"));
|
|
3872
3999
|
}
|
|
3873
4000
|
};
|
|
3874
4001
|
|
|
@@ -4365,7 +4492,8 @@ function initComty(options = {}) {
|
|
|
4365
4492
|
: headers;
|
|
4366
4493
|
}
|
|
4367
4494
|
|
|
4368
|
-
function showAuthPrompt(forbidden = false) {
|
|
4495
|
+
function showAuthPrompt(forbidden = false, onReady = startEditSession) {
|
|
4496
|
+
const guestAllowed = allowGuestComments;
|
|
4369
4497
|
document.querySelector(".comty-auth-backdrop")?.remove();
|
|
4370
4498
|
const opener = document.activeElement;
|
|
4371
4499
|
const backdrop = document.createElement("div");
|
|
@@ -4376,12 +4504,23 @@ function initComty(options = {}) {
|
|
|
4376
4504
|
<span class="comty-auth-icon">${icon('<path d="M21 11.5a8.4 8.4 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7a8.5 8.5 0 1 1 16.1-3.8Z"/>')}</span>
|
|
4377
4505
|
<button type="button" class="comty-auth-close" data-comty-close aria-label="Close sign-in dialog">${icon('<path d="m6 6 12 12M6 18 18 6"/>')}</button>
|
|
4378
4506
|
</div>
|
|
4379
|
-
<h2 id="comty-auth-title">${forbidden ? "Project access required" : "Sign in to comment"}</h2>
|
|
4380
|
-
<p id="comty-auth-description">${forbidden ? "This account is not a member of this project. Ask the project owner for an invite." : "Sign in to share feedback with your team and keep the conversation in one place."}</p>
|
|
4381
|
-
|
|
4382
|
-
<
|
|
4383
|
-
|
|
4384
|
-
|
|
4507
|
+
<h2 id="comty-auth-title">${guestAllowed ? "Leave a comment" : forbidden ? "Project access required" : "Sign in to comment"}</h2>
|
|
4508
|
+
<p id="comty-auth-description">${guestAllowed ? "Continue with your name or sign in with Google." : forbidden ? "This account is not a member of this project. Ask the project owner for an invite." : "Sign in to share feedback with your team and keep the conversation in one place."}</p>
|
|
4509
|
+
${guestAllowed ? `
|
|
4510
|
+
<form class="comty-guest-form">
|
|
4511
|
+
<label for="comty-guest-name">Your name</label>
|
|
4512
|
+
<input id="comty-guest-name" name="name" autocomplete="name" placeholder="Your name" maxlength="80" required aria-describedby="comty-guest-error" />
|
|
4513
|
+
<p id="comty-guest-error" role="alert" hidden></p>
|
|
4514
|
+
<div class="comty-auth-actions comty-guest-actions">
|
|
4515
|
+
<button type="submit" class="comty-auth-primary" data-comty-guest>Continue</button>
|
|
4516
|
+
<span class="comty-auth-or">or</span>
|
|
4517
|
+
<button type="button" data-comty-login>Continue with Google</button>
|
|
4518
|
+
</div>
|
|
4519
|
+
</form>` : `
|
|
4520
|
+
<div class="comty-auth-actions">
|
|
4521
|
+
<button type="button" data-comty-close>${forbidden ? "Close" : "Cancel"}</button>
|
|
4522
|
+
${forbidden ? "" : '<button type="button" class="comty-auth-primary" data-comty-login>Continue to sign in</button>'}
|
|
4523
|
+
</div>`}
|
|
4385
4524
|
</div>`;
|
|
4386
4525
|
const close = () => {
|
|
4387
4526
|
backdrop.remove();
|
|
@@ -4392,11 +4531,12 @@ function initComty(options = {}) {
|
|
|
4392
4531
|
event.stopPropagation();
|
|
4393
4532
|
if (event.key === "Escape") { event.preventDefault(); close(); }
|
|
4394
4533
|
if (event.key === "Tab") {
|
|
4395
|
-
const buttons = [...backdrop.querySelectorAll("button:not(:disabled)")];
|
|
4534
|
+
const buttons = [...backdrop.querySelectorAll("button:not(:disabled), input:not(:disabled)")];
|
|
4396
4535
|
if (!buttons.length) { event.preventDefault(); return; }
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4536
|
+
event.preventDefault();
|
|
4537
|
+
const index = buttons.indexOf(document.activeElement);
|
|
4538
|
+
const next = (index + (event.shiftKey ? -1 : 1) + buttons.length) % buttons.length;
|
|
4539
|
+
buttons[next].focus();
|
|
4400
4540
|
}
|
|
4401
4541
|
});
|
|
4402
4542
|
const loginButton = backdrop.querySelector("[data-comty-login]");
|
|
@@ -4404,15 +4544,62 @@ function initComty(options = {}) {
|
|
|
4404
4544
|
loginButton.onclick = () => {
|
|
4405
4545
|
loginButton.disabled = true;
|
|
4406
4546
|
loginButton.innerText = "Connecting…";
|
|
4407
|
-
backdrop.querySelectorAll("
|
|
4408
|
-
|
|
4547
|
+
backdrop.querySelectorAll("button, input").forEach(control => { control.disabled = true; });
|
|
4548
|
+
try {
|
|
4549
|
+
sessionStorage.setItem(authReturnStorageKey, JSON.stringify({
|
|
4550
|
+
mode: onReady === startEditSession || session.mode === "off" ? "edit" : session.mode,
|
|
4551
|
+
entry: sessionEntry,
|
|
4552
|
+
sidepanelOpen,
|
|
4553
|
+
}));
|
|
4554
|
+
} catch { /* Resume in comment mode if browser storage is unavailable. */ }
|
|
4555
|
+
const destination = new URL("/api/auth/sign-in/google", apiUrl);
|
|
4556
|
+
destination.searchParams.set("callbackURL", loginUrl);
|
|
4557
|
+
setTimeout(() => window.location.assign(guestAllowed ? destination.toString() : loginUrl), 50);
|
|
4409
4558
|
};
|
|
4410
4559
|
}
|
|
4560
|
+
const guestForm = backdrop.querySelector(".comty-guest-form");
|
|
4561
|
+
if (guestForm) guestForm.onsubmit = async event => {
|
|
4562
|
+
event.preventDefault();
|
|
4563
|
+
const nameInput = guestForm.querySelector("input");
|
|
4564
|
+
const error = guestForm.querySelector("[role=alert]");
|
|
4565
|
+
const name = nameInput.value.trim();
|
|
4566
|
+
if (!name) { nameInput.focus(); return; }
|
|
4567
|
+
const controls = [...backdrop.querySelectorAll("button, input")];
|
|
4568
|
+
controls.forEach(control => { control.disabled = true; });
|
|
4569
|
+
error.hidden = true;
|
|
4570
|
+
const continueButton = guestForm.querySelector("[data-comty-guest]");
|
|
4571
|
+
continueButton.textContent = "Connecting…";
|
|
4572
|
+
try {
|
|
4573
|
+
const response = await fetch(`${apiUrl}/api/sdk/guest`, {
|
|
4574
|
+
method: "POST",
|
|
4575
|
+
headers: { "Content-Type": "application/json" },
|
|
4576
|
+
body: JSON.stringify({ projectKey, name }),
|
|
4577
|
+
});
|
|
4578
|
+
const data = await response.json();
|
|
4579
|
+
if (!response.ok) throw new Error(data.error || "Unable to continue. Please try again.");
|
|
4580
|
+
sdkToken = data.token;
|
|
4581
|
+
try { sessionStorage.setItem(tokenStorageKey, sdkToken); } catch { /* Keep the session in memory when browser storage is unavailable. */ }
|
|
4582
|
+
currentUser = data.user;
|
|
4583
|
+
canWrite = true;
|
|
4584
|
+
canManage = false;
|
|
4585
|
+
if (backdrop.isConnected) {
|
|
4586
|
+
close();
|
|
4587
|
+
onReady();
|
|
4588
|
+
}
|
|
4589
|
+
} catch (failure) {
|
|
4590
|
+
if (!backdrop.isConnected) return;
|
|
4591
|
+
error.textContent = failure.message || "Unable to connect. Please try again.";
|
|
4592
|
+
error.hidden = false;
|
|
4593
|
+
controls.forEach(control => { control.disabled = false; });
|
|
4594
|
+
continueButton.textContent = "Continue";
|
|
4595
|
+
nameInput.focus();
|
|
4596
|
+
}
|
|
4597
|
+
};
|
|
4411
4598
|
backdrop.onclick = (event) => {
|
|
4412
4599
|
if (event.target === backdrop) close();
|
|
4413
4600
|
};
|
|
4414
4601
|
root.appendChild(backdrop);
|
|
4415
|
-
(loginButton || backdrop.querySelector("[data-comty-close]")).focus({ preventScroll: true });
|
|
4602
|
+
(backdrop.querySelector("#comty-guest-name") || loginButton || backdrop.querySelector("[data-comty-close]")).focus({ preventScroll: true });
|
|
4416
4603
|
}
|
|
4417
4604
|
|
|
4418
4605
|
function formatBoxValue(value) {
|
|
@@ -4609,7 +4796,8 @@ function initComty(options = {}) {
|
|
|
4609
4796
|
add("background", css.backgroundColor, (tokens.background.length > 0 || !["rgba(0, 0, 0, 0)", "rgb(255, 255, 255)"].includes(css.backgroundColor)));
|
|
4610
4797
|
add("color", css.color);
|
|
4611
4798
|
if (getElementOwnTextSignature(el)) {
|
|
4612
|
-
add("font",
|
|
4799
|
+
add("font-size", css.fontSize);
|
|
4800
|
+
add("font-weight", css.fontWeight);
|
|
4613
4801
|
add("font-family", css.fontFamily);
|
|
4614
4802
|
add("line-height", css.lineHeight);
|
|
4615
4803
|
add("letter-spacing", css.letterSpacing, css.letterSpacing !== "normal");
|
|
@@ -4793,7 +4981,7 @@ function initComty(options = {}) {
|
|
|
4793
4981
|
height: draftBubble.offsetHeight,
|
|
4794
4982
|
};
|
|
4795
4983
|
const pinBounds = draftPin?.getBoundingClientRect();
|
|
4796
|
-
const signature = JSON.stringify([selector, rect, hoverRect, rows, !!selected,
|
|
4984
|
+
const signature = JSON.stringify([selector, rect, hoverRect, rows, !!selected, measureAlt, draftBounds, pinBounds, spacing, innerWidth, innerHeight]);
|
|
4797
4985
|
if (signature === inspectionSignature) return;
|
|
4798
4986
|
inspectionSignature = signature;
|
|
4799
4987
|
inspectionLayer.replaceChildren();
|
|
@@ -4819,36 +5007,34 @@ function initComty(options = {}) {
|
|
|
4819
5007
|
const panel = document.createElement("div");
|
|
4820
5008
|
panel.className = "comty-inspector";
|
|
4821
5009
|
panel.setAttribute("aria-label", "Element properties");
|
|
4822
|
-
if (selected) panel.dataset.selected = "";
|
|
4823
|
-
const
|
|
4824
|
-
const
|
|
4825
|
-
const
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
5010
|
+
if (selected || draftPreview) panel.dataset.selected = "";
|
|
5011
|
+
const priority = ["background", "color", "font-family", "font-size", "font-weight", "margin", "line-height", "letter-spacing"];
|
|
5012
|
+
const orderedRows = [...rows.filter(([key]) => priority.includes(key)).sort((a, b) => priority.indexOf(a[0]) - priority.indexOf(b[0])), ...rows.filter(([key]) => !priority.includes(key))];
|
|
5013
|
+
const labels = { "font-family": "Font", "font-size": "Size", "font-weight": "Weight", "line-height": "Line height", "letter-spacing": "Spacing", "background": "Background", "color": "Color", "margin": "Margin" };
|
|
5014
|
+
const cssProperty = key => ({ radius: "border-radius", background: "background-color" })[key] || key;
|
|
5015
|
+
const copyValue = (value, tokens) => tokens?.length ? `var(${tokens[0]})` : value;
|
|
5016
|
+
const allProperties = `${selector} {\n${rows.map(([key, value, tokens]) => ` ${cssProperty(key)}: ${copyValue(value, tokens)};`).join("\n")}\n}`;
|
|
5017
|
+
const copyButton = (value, label) => `<button type="button" class="comty-inspector-copy" data-copy="${escapeInspectorText(value)}" aria-label="${escapeInspectorText(label)}" title="${escapeInspectorText(label)}"><i class="comty-property-copy-icon" aria-hidden="true">${copyIcon}${checkIcon}</i></button>`;
|
|
5018
|
+
panel.innerHTML = `<div class="comty-inspector-head"><span class="comty-inspector-selector" dir="rtl" title="${escapeInspectorText(selector)}"><bdi dir="ltr">${escapeInspectorText(selector)}</bdi></span>${copyButton(allProperties, "Copy all properties")}</div><div class="comty-inspector-details">${orderedRows.map(([key, value, tokens]) => {
|
|
5019
|
+
const label = labels[key] || key.replaceAll("-", " ").replace(/^./, c => c.toUpperCase());
|
|
5020
|
+
const isColor = key === "background" || key === "color";
|
|
5021
|
+
const picker = isColor ? "color" : key.startsWith("font-") || ["line-height", "letter-spacing"].includes(key) ? "font" : "";
|
|
5022
|
+
const displayValue = isColor ? colorDetails(value)?.hex || value : value;
|
|
5023
|
+
const valueMarkup = `${isColor ? '<span class="comty-inspector-swatch" aria-hidden="true"></span>' : ""}<span class="comty-inspector-text">${tokens?.length ? `<span class="comty-inspector-tokens">${tokens.map(escapeInspectorText).join(", ")}</span><span class="comty-inspector-secondary">${escapeInspectorText(displayValue)}</span>` : escapeInspectorText(displayValue)}</span>`;
|
|
5024
|
+
const valueElement = picker && selected
|
|
5025
|
+
? `<button type="button" class="comty-inspector-value comty-inspector-picker" data-picker="${picker}" data-property="${key}" aria-label="Open ${isColor ? key + " colour" : "typography"} details">${valueMarkup}</button>`
|
|
5026
|
+
: `<span class="comty-inspector-value" data-property="${key}">${valueMarkup}</span>`;
|
|
5027
|
+
return `<div class="comty-inspector-row"><span class="comty-inspector-label">${label}</span>${valueElement}${copyButton(tokens?.length ? tokens[0].replace(/^--/, "") : value, `Copy ${label}`)}</div>`;
|
|
5028
|
+
}).join("")}</div><span class="comty-property-feedback" role="status"></span>`;
|
|
5029
|
+
for (const swatch of panel.querySelectorAll('.comty-inspector-swatch')) {
|
|
5030
|
+
const key = swatch.parentElement.dataset.property;
|
|
5031
|
+
swatch.style.backgroundColor = rows.find(([name]) => name === key)?.[1];
|
|
4835
5032
|
}
|
|
4836
5033
|
panel.onclick = async (event) => {
|
|
4837
5034
|
const button = event.target.closest("button");
|
|
4838
5035
|
if (!button) return;
|
|
4839
|
-
if (button.
|
|
4840
|
-
|
|
4841
|
-
renderInspector();
|
|
4842
|
-
} else if (button.dataset.picker) {
|
|
4843
|
-
openPropertyPanel(button.dataset.picker, target, button.dataset.property, button);
|
|
4844
|
-
} else {
|
|
4845
|
-
try {
|
|
4846
|
-
await navigator.clipboard.writeText(button.dataset.copy);
|
|
4847
|
-
button.title = "Copied";
|
|
4848
|
-
} catch {
|
|
4849
|
-
button.title = "Clipboard unavailable — select the value to copy";
|
|
4850
|
-
}
|
|
4851
|
-
}
|
|
5036
|
+
if (button.dataset.picker) openPropertyPanel(button.dataset.picker, target, button.dataset.property, button);
|
|
5037
|
+
else await copyPropertyValue(button, panel.querySelector(".comty-property-feedback"));
|
|
4852
5038
|
};
|
|
4853
5039
|
inspectionLayer.appendChild(panel);
|
|
4854
5040
|
const bounds = panel.getBoundingClientRect();
|
|
@@ -4866,8 +5052,24 @@ function initComty(options = {}) {
|
|
|
4866
5052
|
}
|
|
4867
5053
|
return;
|
|
4868
5054
|
}
|
|
4869
|
-
|
|
4870
|
-
|
|
5055
|
+
const inset = 8, gap = 12;
|
|
5056
|
+
const clampX = x => Math.max(inset, Math.min(x, innerWidth - bounds.width - inset));
|
|
5057
|
+
const clampY = y => Math.max(inset, Math.min(y, innerHeight - bounds.height - inset));
|
|
5058
|
+
const x = clampX(rect.left), y = clampY(rect.top);
|
|
5059
|
+
const candidates = [
|
|
5060
|
+
{ left: x, top: rect.bottom + gap },
|
|
5061
|
+
{ left: rect.right + gap, top: y },
|
|
5062
|
+
{ left: rect.left - bounds.width - gap, top: y },
|
|
5063
|
+
{ left: x, top: rect.top - bounds.height - gap },
|
|
5064
|
+
];
|
|
5065
|
+
let position = candidates.find(({ left, top }) => left >= inset && top >= inset && left + bounds.width <= innerWidth - inset && top + bounds.height <= innerHeight - inset);
|
|
5066
|
+
if (!position) {
|
|
5067
|
+
// A viewport-sized target leaves no outside space; minimize the covered area.
|
|
5068
|
+
const overlap = ({ left, top }) => Math.max(0, Math.min(left + bounds.width, rect.right) - Math.max(left, rect.left)) * Math.max(0, Math.min(top + bounds.height, rect.bottom) - Math.max(top, rect.top));
|
|
5069
|
+
position = candidates.map(({ left, top }) => ({ left: clampX(left), top: clampY(top) })).sort((a, b) => overlap(a) - overlap(b))[0];
|
|
5070
|
+
}
|
|
5071
|
+
panel.style.left = `${position.left}px`;
|
|
5072
|
+
panel.style.top = `${position.top}px`;
|
|
4871
5073
|
}
|
|
4872
5074
|
|
|
4873
5075
|
function endSession() {
|
|
@@ -5256,15 +5458,6 @@ function initComty(options = {}) {
|
|
|
5256
5458
|
return element;
|
|
5257
5459
|
}
|
|
5258
5460
|
|
|
5259
|
-
async function waitForCommentTargetReady(comment) {
|
|
5260
|
-
const el = resolveCommentElement(comment);
|
|
5261
|
-
if (!el) return false;
|
|
5262
|
-
|
|
5263
|
-
await waitForCustomElements(el);
|
|
5264
|
-
await waitForPaint();
|
|
5265
|
-
return true;
|
|
5266
|
-
}
|
|
5267
|
-
|
|
5268
5461
|
function getDeviceContext() {
|
|
5269
5462
|
const width = window.innerWidth;
|
|
5270
5463
|
const height = window.innerHeight;
|
|
@@ -5339,125 +5532,44 @@ function initComty(options = {}) {
|
|
|
5339
5532
|
if (!session.edit || hadDraft) hideInspector();
|
|
5340
5533
|
document
|
|
5341
5534
|
.querySelectorAll(
|
|
5342
|
-
".comty-bubble, .comty-create-bubble, .comty-temp-pin, .comty-popover",
|
|
5535
|
+
".comty-bubble:not([data-comty-capture-keep]), .comty-create-bubble, .comty-temp-pin, .comty-popover",
|
|
5343
5536
|
)
|
|
5344
5537
|
.forEach((x) => x.remove());
|
|
5345
5538
|
}
|
|
5346
5539
|
|
|
5347
|
-
const {
|
|
5348
|
-
|
|
5349
|
-
async function buildCommentScreenshotFormData(payload) {
|
|
5350
|
-
const formData = new FormData();
|
|
5351
|
-
|
|
5352
|
-
Object.entries(payload).forEach(([key, value]) => {
|
|
5353
|
-
if (value === undefined || value === null) return;
|
|
5354
|
-
formData.append(key, String(value));
|
|
5355
|
-
});
|
|
5356
|
-
|
|
5357
|
-
try {
|
|
5358
|
-
await waitForCommentTargetReady(payload);
|
|
5359
|
-
const screenshot = await captureViewportScreenshot();
|
|
5360
|
-
if (screenshot) {
|
|
5361
|
-
const extension =
|
|
5362
|
-
screenshot.blob.type === "image/svg+xml"
|
|
5363
|
-
? "svg"
|
|
5364
|
-
: screenshot.blob.type === "image/jpeg"
|
|
5365
|
-
? "jpg"
|
|
5366
|
-
: screenshot.blob.type === "image/png"
|
|
5367
|
-
? "png"
|
|
5368
|
-
: "webp";
|
|
5369
|
-
formData.append(
|
|
5370
|
-
"screenshot",
|
|
5371
|
-
screenshot.blob,
|
|
5372
|
-
`comment.${extension}`,
|
|
5373
|
-
);
|
|
5374
|
-
formData.append("screenshotWidth", String(screenshot.width));
|
|
5375
|
-
formData.append("screenshotHeight", String(screenshot.height));
|
|
5376
|
-
}
|
|
5377
|
-
} catch (error) {
|
|
5378
|
-
console.error("Comty: screenshot capture failed", error);
|
|
5379
|
-
}
|
|
5380
|
-
|
|
5381
|
-
return formData;
|
|
5382
|
-
}
|
|
5540
|
+
const { captureViewportScreenshot } = createScreenshotCapture({ apiUrl, projectKey, getHeaders: getSdkHeaders });
|
|
5383
5541
|
|
|
5384
|
-
async function uploadCommentScreenshot(
|
|
5385
|
-
commentId,
|
|
5386
|
-
payload,
|
|
5387
|
-
captureNodes = [],
|
|
5388
|
-
) {
|
|
5542
|
+
async function uploadCommentScreenshot(comment, context, point) {
|
|
5389
5543
|
try {
|
|
5390
|
-
const
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
if (!formData.has("screenshot")) return;
|
|
5399
|
-
|
|
5544
|
+
const screenshot = await captureViewportScreenshot({ context, annotation: () => mountCaptureAnnotation(comment, point) });
|
|
5545
|
+
if (!screenshot) return;
|
|
5546
|
+
const formData = new FormData();
|
|
5547
|
+
formData.append("projectKey", projectKey);
|
|
5548
|
+
formData.append("screenshot", screenshot.blob, screenshot.blob.type === "image/jpeg" ? "comment.jpg" : "comment.png");
|
|
5549
|
+
formData.append("screenshotWidth", String(screenshot.width));
|
|
5550
|
+
formData.append("screenshotHeight", String(screenshot.height));
|
|
5400
5551
|
const response = await fetch(
|
|
5401
|
-
`${apiUrl}/api/comments/${encodeURIComponent(
|
|
5402
|
-
{
|
|
5403
|
-
method: "POST",
|
|
5404
|
-
headers: getSdkHeaders(),
|
|
5405
|
-
body: formData,
|
|
5406
|
-
},
|
|
5552
|
+
`${apiUrl}/api/comments/${encodeURIComponent(comment.id)}/screenshot`,
|
|
5553
|
+
{ method: "POST", headers: getSdkHeaders(), body: formData },
|
|
5407
5554
|
);
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
await loadComments({ force: true });
|
|
5555
|
+
if (!response.ok) throw new Error(`Screenshot upload failed (${response.status})`);
|
|
5556
|
+
// Updating metadata must not redraw the page or interrupt a new draft.
|
|
5557
|
+
const { screenshotUrl } = await response.json();
|
|
5558
|
+
const cached = cachedComments.find(item => String(item.id) === String(comment.id));
|
|
5559
|
+
if (cached) Object.assign(cached, {
|
|
5560
|
+
screenshot_url: screenshotUrl,
|
|
5561
|
+
screenshot_width: screenshot.width,
|
|
5562
|
+
screenshot_height: screenshot.height,
|
|
5563
|
+
});
|
|
5418
5564
|
} catch (error) {
|
|
5419
|
-
console.
|
|
5420
|
-
|
|
5421
|
-
}
|
|
5422
|
-
}
|
|
5423
|
-
|
|
5424
|
-
function createCaptureAnnotation(pin, bubble, content) {
|
|
5425
|
-
const capturePin = pin.cloneNode(false);
|
|
5426
|
-
capturePin.dataset.comtyCaptureKeep = "true";
|
|
5427
|
-
capturePin.classList.add("comty-capture-annotation");
|
|
5428
|
-
capturePin.classList.remove("comty-temp-pin");
|
|
5429
|
-
capturePin.style.display = "grid";
|
|
5430
|
-
capturePin.style.pointerEvents = "none";
|
|
5431
|
-
capturePin.style.animation = "none";
|
|
5432
|
-
capturePin.style.opacity = "1";
|
|
5433
|
-
capturePin.style.transform = "translate(-50%, -50%)";
|
|
5434
|
-
setPinIcon(capturePin, currentUser || { name: "You" });
|
|
5435
|
-
|
|
5436
|
-
const captureBubble = document.createElement("div");
|
|
5437
|
-
captureBubble.className =
|
|
5438
|
-
"comty-bubble comty-hover-bubble comty-capture-annotation";
|
|
5439
|
-
captureBubble.dataset.comtyCaptureKeep = "true";
|
|
5440
|
-
captureBubble.style.left = bubble.style.left;
|
|
5441
|
-
captureBubble.style.top = bubble.style.top;
|
|
5442
|
-
captureBubble.style.display = "block";
|
|
5443
|
-
captureBubble.style.pointerEvents = "none";
|
|
5444
|
-
captureBubble.style.animation = "none";
|
|
5445
|
-
captureBubble.style.opacity = "1";
|
|
5446
|
-
captureBubble.style.transform = "none";
|
|
5447
|
-
|
|
5448
|
-
const contentEl = document.createElement("div");
|
|
5449
|
-
contentEl.className = "comty-bubble-content";
|
|
5450
|
-
contentEl.innerText = content;
|
|
5451
|
-
captureBubble.appendChild(contentEl);
|
|
5452
|
-
|
|
5453
|
-
document.body.appendChild(capturePin);
|
|
5454
|
-
document.body.appendChild(captureBubble);
|
|
5455
|
-
return [capturePin, captureBubble];
|
|
5565
|
+
console.warn("Comty: comment saved, screenshot unavailable", error);
|
|
5566
|
+
}
|
|
5456
5567
|
}
|
|
5457
5568
|
|
|
5458
5569
|
function addLocalComment(commentId, payload) {
|
|
5459
5570
|
const comment = {
|
|
5460
5571
|
id: commentId,
|
|
5572
|
+
pending: commentId.startsWith("pending-"),
|
|
5461
5573
|
project_id: projectId,
|
|
5462
5574
|
domain: payload.host || window.location.host,
|
|
5463
5575
|
page_path: payload.pagePath || window.location.pathname,
|
|
@@ -5465,6 +5577,7 @@ function initComty(options = {}) {
|
|
|
5465
5577
|
rel_x: payload.relX,
|
|
5466
5578
|
rel_y: payload.relY,
|
|
5467
5579
|
content: payload.content || "",
|
|
5580
|
+
link_url: payload.linkUrl || null,
|
|
5468
5581
|
created_at: new Date().toISOString(),
|
|
5469
5582
|
dom_element: payload.domElement || null,
|
|
5470
5583
|
viewport: payload.viewport || null,
|
|
@@ -5476,7 +5589,9 @@ function initComty(options = {}) {
|
|
|
5476
5589
|
screenshot_width: null,
|
|
5477
5590
|
screenshot_height: null,
|
|
5478
5591
|
anchor_context: payload.anchorContext || null,
|
|
5479
|
-
author_user_id: currentUser?.id || null,
|
|
5592
|
+
author_user_id: currentUser?.isGuest ? null : currentUser?.id || null,
|
|
5593
|
+
guest_author_id: currentUser?.isGuest ? currentUser.id : null,
|
|
5594
|
+
guest_author_name: currentUser?.isGuest ? currentUser.name : null,
|
|
5480
5595
|
author_name: currentUser?.name || null,
|
|
5481
5596
|
author_avatar_url: currentUser?.avatarUrl || null,
|
|
5482
5597
|
replies: [],
|
|
@@ -5491,6 +5606,7 @@ function initComty(options = {}) {
|
|
|
5491
5606
|
];
|
|
5492
5607
|
renderSidepanelComments();
|
|
5493
5608
|
void renderComments(cachedComments, { retry: false });
|
|
5609
|
+
return comment;
|
|
5494
5610
|
}
|
|
5495
5611
|
|
|
5496
5612
|
function getCommentAuthorName(comment) {
|
|
@@ -5500,8 +5616,7 @@ function initComty(options = {}) {
|
|
|
5500
5616
|
function isOwnCommentEntry(entry) {
|
|
5501
5617
|
return Boolean(
|
|
5502
5618
|
currentUser?.id &&
|
|
5503
|
-
entry?.author_user_id
|
|
5504
|
-
entry.author_user_id === currentUser.id,
|
|
5619
|
+
(currentUser.isGuest ? entry?.guest_author_id : entry?.author_user_id) === currentUser.id,
|
|
5505
5620
|
);
|
|
5506
5621
|
}
|
|
5507
5622
|
|
|
@@ -5577,7 +5692,7 @@ function initComty(options = {}) {
|
|
|
5577
5692
|
|
|
5578
5693
|
function createThreadItem(
|
|
5579
5694
|
entry,
|
|
5580
|
-
{ showMore = isOwnCommentEntry(entry) } = {},
|
|
5695
|
+
{ showMore = isOwnCommentEntry(entry), showLink = true } = {},
|
|
5581
5696
|
) {
|
|
5582
5697
|
const item = document.createElement("div");
|
|
5583
5698
|
item.className = "comty-thread-item";
|
|
@@ -5612,15 +5727,51 @@ function initComty(options = {}) {
|
|
|
5612
5727
|
|
|
5613
5728
|
const content = document.createElement("div");
|
|
5614
5729
|
content.className = "comty-bubble-content";
|
|
5615
|
-
content
|
|
5730
|
+
renderCommentContent(content, entry?.content || "");
|
|
5616
5731
|
|
|
5617
5732
|
body.appendChild(author);
|
|
5618
5733
|
body.appendChild(content);
|
|
5734
|
+
if (showLink && entry?.link_url) body.appendChild(createCommentLink(entry.link_url));
|
|
5619
5735
|
item.appendChild(body);
|
|
5620
5736
|
|
|
5621
5737
|
return { item, body, content, moreButton };
|
|
5622
5738
|
}
|
|
5623
5739
|
|
|
5740
|
+
function renderCommentContent(element, value) {
|
|
5741
|
+
const content = String(value ?? "");
|
|
5742
|
+
const nodes = [];
|
|
5743
|
+
let previousEnd = 0;
|
|
5744
|
+
for (const match of content.matchAll(/https?:\/\/[^\s<>"']+/gi)) {
|
|
5745
|
+
const url = match[0].replace(/[.,!?;:]+$/, "");
|
|
5746
|
+
if (!url) continue;
|
|
5747
|
+
let parsed;
|
|
5748
|
+
try { parsed = new URL(url); } catch { continue; }
|
|
5749
|
+
if (!["http:", "https:"].includes(parsed.protocol) || !parsed.hostname) continue;
|
|
5750
|
+
nodes.push(document.createTextNode(content.slice(previousEnd, match.index)));
|
|
5751
|
+
const link = document.createElement("a");
|
|
5752
|
+
link.href = url;
|
|
5753
|
+
link.target = "_blank";
|
|
5754
|
+
link.rel = "noopener noreferrer";
|
|
5755
|
+
link.textContent = url;
|
|
5756
|
+
nodes.push(link);
|
|
5757
|
+
previousEnd = match.index + url.length;
|
|
5758
|
+
}
|
|
5759
|
+
nodes.push(document.createTextNode(content.slice(previousEnd)));
|
|
5760
|
+
element.replaceChildren(...nodes);
|
|
5761
|
+
}
|
|
5762
|
+
|
|
5763
|
+
function createCommentLink(url) {
|
|
5764
|
+
const link = document.createElement("a");
|
|
5765
|
+
link.className = "comty-comment-link";
|
|
5766
|
+
link.href = url;
|
|
5767
|
+
link.target = "_blank";
|
|
5768
|
+
link.rel = "noopener noreferrer";
|
|
5769
|
+
link.title = url;
|
|
5770
|
+
link.setAttribute("aria-label", `Open attached link: ${url}`);
|
|
5771
|
+
link.textContent = url;
|
|
5772
|
+
return link;
|
|
5773
|
+
}
|
|
5774
|
+
|
|
5624
5775
|
function setPinIcon(pin, comment = null) {
|
|
5625
5776
|
if (comment) {
|
|
5626
5777
|
pin.classList.add("comty-pin-with-avatar");
|
|
@@ -5637,6 +5788,54 @@ function initComty(options = {}) {
|
|
|
5637
5788
|
`;
|
|
5638
5789
|
}
|
|
5639
5790
|
|
|
5791
|
+
function createCommentPin(comment, x, y) {
|
|
5792
|
+
const pin = document.createElement("button");
|
|
5793
|
+
pin.className = "comty-pin";
|
|
5794
|
+
pin.type = "button";
|
|
5795
|
+
pin.style.left = `${x}px`;
|
|
5796
|
+
pin.style.top = `${y}px`;
|
|
5797
|
+
setPinIcon(pin, comment);
|
|
5798
|
+
const statusDot = document.createElement("span");
|
|
5799
|
+
statusDot.className = "comty-pin-status-dot";
|
|
5800
|
+
applyStatusDot(statusDot, comment.status);
|
|
5801
|
+
pin.appendChild(statusDot);
|
|
5802
|
+
return pin;
|
|
5803
|
+
}
|
|
5804
|
+
|
|
5805
|
+
function createCommentPreview(comment) {
|
|
5806
|
+
const bubble = document.createElement("div");
|
|
5807
|
+
bubble.className = "comty-bubble comty-hover-bubble";
|
|
5808
|
+
const thread = document.createElement("div");
|
|
5809
|
+
thread.className = "comty-thread";
|
|
5810
|
+
thread.appendChild(createThreadItem(comment, { showMore: false, showLink: false }).item);
|
|
5811
|
+
bubble.appendChild(thread);
|
|
5812
|
+
return bubble;
|
|
5813
|
+
}
|
|
5814
|
+
|
|
5815
|
+
function mountCaptureAnnotation(comment, { x, y }) {
|
|
5816
|
+
const host = document.createElement("div");
|
|
5817
|
+
host.dataset.comtyCaptureExclude = "";
|
|
5818
|
+
host.setAttribute("aria-hidden", "true");
|
|
5819
|
+
host.inert = true;
|
|
5820
|
+
// Ancestor opacity hides measurement without changing the components'
|
|
5821
|
+
// computed opacity. Nothing flashes or intercepts input during cloning.
|
|
5822
|
+
Object.assign(host.style, { position: "fixed", inset: "0", opacity: "0", pointerEvents: "none" });
|
|
5823
|
+
const pin = createCommentPin(comment, x, y);
|
|
5824
|
+
const bubble = createCommentPreview(comment);
|
|
5825
|
+
for (const element of [pin, bubble]) {
|
|
5826
|
+
element.dataset.comtyCaptureKeep = "";
|
|
5827
|
+
element.style.setProperty("animation", "none", "important");
|
|
5828
|
+
element.style.setProperty("transition", "none", "important");
|
|
5829
|
+
element.style.setProperty("pointer-events", "none", "important");
|
|
5830
|
+
}
|
|
5831
|
+
pin.style.display = "grid";
|
|
5832
|
+
bubble.style.display = "block";
|
|
5833
|
+
host.append(pin, bubble);
|
|
5834
|
+
root.appendChild(host);
|
|
5835
|
+
positionCommentBubble(bubble, x, y);
|
|
5836
|
+
return { elements: [pin, bubble], dispose: () => host.remove() };
|
|
5837
|
+
}
|
|
5838
|
+
|
|
5640
5839
|
function createCommentAuthor(comment) {
|
|
5641
5840
|
const author = document.createElement("div");
|
|
5642
5841
|
author.className = "comty-comment-author";
|
|
@@ -5687,25 +5886,86 @@ function initComty(options = {}) {
|
|
|
5687
5886
|
const actions = document.createElement("div");
|
|
5688
5887
|
actions.className = "comty-create-actions";
|
|
5689
5888
|
|
|
5889
|
+
const linkBtn = document.createElement("button");
|
|
5890
|
+
linkBtn.type = "button";
|
|
5891
|
+
linkBtn.className = "comty-create-link-button";
|
|
5892
|
+
linkBtn.setAttribute("aria-label", "Add link");
|
|
5893
|
+
linkBtn.innerHTML = `
|
|
5894
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
5895
|
+
<path d="M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
|
5896
|
+
<path d="M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
|
5897
|
+
</svg>
|
|
5898
|
+
`;
|
|
5899
|
+
|
|
5900
|
+
const linkEditor = document.createElement("div");
|
|
5901
|
+
linkEditor.className = "comty-create-link-editor";
|
|
5902
|
+
linkEditor.inert = true;
|
|
5903
|
+
linkEditor.setAttribute("aria-hidden", "true");
|
|
5904
|
+
const linkInput = document.createElement("input");
|
|
5905
|
+
linkInput.type = "url";
|
|
5906
|
+
linkInput.placeholder = "Paste a link";
|
|
5907
|
+
linkInput.setAttribute("aria-label", "Link URL");
|
|
5908
|
+
const linkInsert = document.createElement("button");
|
|
5909
|
+
linkInsert.type = "button";
|
|
5910
|
+
linkInsert.textContent = "Add";
|
|
5911
|
+
const linkCancel = document.createElement("button");
|
|
5912
|
+
linkCancel.type = "button";
|
|
5913
|
+
linkCancel.setAttribute("aria-label", "Cancel link");
|
|
5914
|
+
linkCancel.textContent = "×";
|
|
5915
|
+
const linkField = document.createElement("div");
|
|
5916
|
+
linkField.className = "comty-create-link-field";
|
|
5917
|
+
linkField.append(linkInput, linkCancel);
|
|
5918
|
+
linkEditor.append(linkField, linkInsert);
|
|
5919
|
+
|
|
5920
|
+
const attachment = document.createElement("div");
|
|
5921
|
+
attachment.className = "comty-create-attachment";
|
|
5922
|
+
const attachmentLink = document.createElement("a");
|
|
5923
|
+
attachmentLink.className = "comty-comment-link";
|
|
5924
|
+
attachmentLink.target = "_blank";
|
|
5925
|
+
attachmentLink.rel = "noopener noreferrer";
|
|
5926
|
+
const removeAttachment = document.createElement("button");
|
|
5927
|
+
removeAttachment.type = "button";
|
|
5928
|
+
removeAttachment.setAttribute("aria-label", "Remove attached link");
|
|
5929
|
+
removeAttachment.textContent = "×";
|
|
5930
|
+
const attachmentChip = document.createElement("div");
|
|
5931
|
+
attachmentChip.className = "comty-create-attachment-chip";
|
|
5932
|
+
attachmentChip.append(attachmentLink, removeAttachment);
|
|
5933
|
+
attachment.appendChild(attachmentChip);
|
|
5934
|
+
let attachedLink = "";
|
|
5935
|
+
|
|
5690
5936
|
const submitBtn = document.createElement("button");
|
|
5937
|
+
submitBtn.type = "button";
|
|
5691
5938
|
submitBtn.className = "comty-btn-submit";
|
|
5692
5939
|
submitBtn.setAttribute("aria-label", "Post comment");
|
|
5940
|
+
submitBtn.disabled = true;
|
|
5693
5941
|
submitBtn.innerHTML = `
|
|
5694
5942
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
5695
5943
|
<path d="M12 19V5M5 12l7-7 7 7"/>
|
|
5696
5944
|
</svg>
|
|
5697
5945
|
`;
|
|
5698
5946
|
|
|
5947
|
+
const editorDraft = draftAnchor;
|
|
5699
5948
|
const submit = async () => {
|
|
5700
5949
|
const content = textarea.value.trim();
|
|
5701
|
-
if (!content) return;
|
|
5950
|
+
if (!content || textarea.disabled) return;
|
|
5702
5951
|
|
|
5703
5952
|
submitBtn.disabled = true;
|
|
5704
5953
|
textarea.disabled = true;
|
|
5954
|
+
linkBtn.disabled = true;
|
|
5705
5955
|
|
|
5706
5956
|
payload.content = content;
|
|
5957
|
+
payload.linkUrl = attachedLink || null;
|
|
5707
5958
|
textarea.textContent = content;
|
|
5708
5959
|
|
|
5960
|
+
const context = captureContext();
|
|
5961
|
+
const point = getPinPoint(pin);
|
|
5962
|
+
const pendingId = `pending-${crypto.randomUUID()}`;
|
|
5963
|
+
// Paint the comment immediately. The network save never waits for capture work.
|
|
5964
|
+
bubble.remove();
|
|
5965
|
+
pin.remove();
|
|
5966
|
+
if (draftAnchor === editorDraft) draftAnchor = null;
|
|
5967
|
+
hideInspector();
|
|
5968
|
+
addLocalComment(pendingId, payload);
|
|
5709
5969
|
let post;
|
|
5710
5970
|
try {
|
|
5711
5971
|
post = await fetch(`${apiUrl}/api/comments`, {
|
|
@@ -5713,35 +5973,32 @@ function initComty(options = {}) {
|
|
|
5713
5973
|
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
5714
5974
|
body: JSON.stringify(payload),
|
|
5715
5975
|
});
|
|
5976
|
+
if (!post.ok) throw new Error(`Comment save failed (${post.status})`);
|
|
5977
|
+
const data = await post.json();
|
|
5978
|
+
if (!data?.id) throw new Error("Comment save returned no ID");
|
|
5979
|
+
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
5980
|
+
if (context.url === location.href) {
|
|
5981
|
+
const comment = addLocalComment(String(data.id), payload);
|
|
5982
|
+
// Start only after the save and immediate UI update; the capture yields to input.
|
|
5983
|
+
void uploadCommentScreenshot(comment, context, point);
|
|
5984
|
+
} else void loadComments({ force: true });
|
|
5716
5985
|
} catch (error) {
|
|
5717
5986
|
console.error("Comty: save failed", error);
|
|
5987
|
+
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
5988
|
+
renderSidepanelComments();
|
|
5989
|
+
void renderComments(cachedComments, { retry: false });
|
|
5990
|
+
// Keep the original editor and its text available for retry, including attachments.
|
|
5991
|
+
if (!draftAnchor) draftAnchor = editorDraft;
|
|
5992
|
+
root.append(pin, bubble);
|
|
5718
5993
|
submitBtn.disabled = false;
|
|
5719
5994
|
textarea.disabled = false;
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
textarea.
|
|
5727
|
-
return;
|
|
5728
|
-
}
|
|
5729
|
-
|
|
5730
|
-
const data = await post.json();
|
|
5731
|
-
const commentId = data?.id;
|
|
5732
|
-
const captureNodes = commentId
|
|
5733
|
-
? createCaptureAnnotation(pin, bubble, content)
|
|
5734
|
-
: [];
|
|
5735
|
-
bubble.remove();
|
|
5736
|
-
pin.remove();
|
|
5737
|
-
draftAnchor = null;
|
|
5738
|
-
hideInspector();
|
|
5739
|
-
if (commentId) {
|
|
5740
|
-
addLocalComment(String(commentId), payload);
|
|
5741
|
-
void loadComments({ force: true });
|
|
5742
|
-
void uploadCommentScreenshot(String(commentId), payload, captureNodes);
|
|
5743
|
-
} else {
|
|
5744
|
-
void loadComments({ force: true });
|
|
5995
|
+
linkBtn.disabled = false;
|
|
5996
|
+
const errorMessage = document.createElement("p");
|
|
5997
|
+
errorMessage.setAttribute("role", "alert");
|
|
5998
|
+
errorMessage.textContent = "Comment not saved. Your draft is ready to retry.";
|
|
5999
|
+
bubble.querySelector('[role="alert"]')?.remove();
|
|
6000
|
+
bubble.append(errorMessage);
|
|
6001
|
+
if (bubble.isConnected) textarea.focus();
|
|
5745
6002
|
}
|
|
5746
6003
|
};
|
|
5747
6004
|
|
|
@@ -5750,8 +6007,63 @@ function initComty(options = {}) {
|
|
|
5750
6007
|
submit();
|
|
5751
6008
|
};
|
|
5752
6009
|
|
|
6010
|
+
const closeLinkEditor = () => {
|
|
6011
|
+
bubble.classList.remove("comty-create-link-open");
|
|
6012
|
+
linkEditor.inert = true;
|
|
6013
|
+
linkEditor.setAttribute("aria-hidden", "true");
|
|
6014
|
+
linkInput.value = "";
|
|
6015
|
+
linkInput.removeAttribute("aria-invalid");
|
|
6016
|
+
linkInput.setCustomValidity("");
|
|
6017
|
+
textarea.focus();
|
|
6018
|
+
};
|
|
6019
|
+
linkBtn.onclick = () => {
|
|
6020
|
+
if (!bubble.classList.contains("comty-create-expanded")) return;
|
|
6021
|
+
bubble.classList.add("comty-create-link-open");
|
|
6022
|
+
linkEditor.inert = false;
|
|
6023
|
+
linkEditor.setAttribute("aria-hidden", "false");
|
|
6024
|
+
linkInput.focus();
|
|
6025
|
+
};
|
|
6026
|
+
linkCancel.onclick = closeLinkEditor;
|
|
6027
|
+
linkInput.oninput = () => {
|
|
6028
|
+
linkInput.removeAttribute("aria-invalid");
|
|
6029
|
+
linkInput.setCustomValidity("");
|
|
6030
|
+
};
|
|
6031
|
+
linkInput.onkeydown = (event) => {
|
|
6032
|
+
if (event.key === "Enter") { event.preventDefault(); linkInsert.click(); }
|
|
6033
|
+
if (event.key === "Escape") { event.preventDefault(); closeLinkEditor(); }
|
|
6034
|
+
};
|
|
6035
|
+
linkInsert.onclick = () => {
|
|
6036
|
+
const value = linkInput.value.trim();
|
|
6037
|
+
const candidate = /^[a-z][a-z0-9+.-]*:/i.test(value) ? value : `https://${value}`;
|
|
6038
|
+
let url;
|
|
6039
|
+
try { url = new URL(candidate); } catch { /* Report below. */ }
|
|
6040
|
+
if (!url || !["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password || /\s/.test(value) || String(url).length > 2048) {
|
|
6041
|
+
linkInput.setAttribute("aria-invalid", "true");
|
|
6042
|
+
linkInput.setCustomValidity("Enter a valid HTTP(S) URL.");
|
|
6043
|
+
linkInput.reportValidity();
|
|
6044
|
+
linkInput.focus();
|
|
6045
|
+
return;
|
|
6046
|
+
}
|
|
6047
|
+
attachedLink = String(url);
|
|
6048
|
+
attachmentLink.href = attachedLink;
|
|
6049
|
+
attachmentLink.title = attachedLink;
|
|
6050
|
+
attachmentLink.textContent = attachedLink;
|
|
6051
|
+
bubble.classList.add("comty-create-has-attachment");
|
|
6052
|
+
closeLinkEditor();
|
|
6053
|
+
const point = getPinPoint(pin);
|
|
6054
|
+
positionCommentBubble(bubble, point.x, point.y);
|
|
6055
|
+
renderInspector();
|
|
6056
|
+
};
|
|
6057
|
+
|
|
6058
|
+
removeAttachment.onclick = () => {
|
|
6059
|
+
attachedLink = "";
|
|
6060
|
+
bubble.classList.remove("comty-create-has-attachment");
|
|
6061
|
+
textarea.focus();
|
|
6062
|
+
textarea.oninput();
|
|
6063
|
+
};
|
|
6064
|
+
|
|
5753
6065
|
textarea.onkeydown = (e) => {
|
|
5754
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
6066
|
+
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
|
5755
6067
|
e.preventDefault();
|
|
5756
6068
|
submit();
|
|
5757
6069
|
}
|
|
@@ -5762,31 +6074,20 @@ function initComty(options = {}) {
|
|
|
5762
6074
|
};
|
|
5763
6075
|
|
|
5764
6076
|
textarea.oninput = () => {
|
|
5765
|
-
textarea.
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
bubble.style.borderRadius = "16px";
|
|
5771
|
-
bubble.style.alignItems = "flex-end";
|
|
5772
|
-
} else {
|
|
5773
|
-
bubble.style.borderRadius = "999px";
|
|
5774
|
-
bubble.style.alignItems = "center";
|
|
5775
|
-
}
|
|
5776
|
-
|
|
5777
|
-
if (textarea.value.trim().length > 0) {
|
|
5778
|
-
submitBtn.classList.add("comty-btn-active");
|
|
5779
|
-
} else {
|
|
5780
|
-
submitBtn.classList.remove("comty-btn-active");
|
|
5781
|
-
}
|
|
6077
|
+
const expanded = textarea.value.length > 0 || Boolean(attachedLink);
|
|
6078
|
+
bubble.classList.toggle("comty-create-expanded", expanded);
|
|
6079
|
+
if (!expanded && bubble.classList.contains("comty-create-link-open")) closeLinkEditor();
|
|
6080
|
+
submitBtn.disabled = !textarea.value.trim();
|
|
6081
|
+
submitBtn.classList.toggle("comty-btn-active", !submitBtn.disabled);
|
|
5782
6082
|
const point = getPinPoint(pin);
|
|
5783
6083
|
positionCommentBubble(bubble, point.x, point.y);
|
|
5784
6084
|
renderInspector();
|
|
5785
6085
|
};
|
|
5786
6086
|
|
|
5787
|
-
actions.
|
|
6087
|
+
actions.append(linkBtn, submitBtn, linkEditor);
|
|
5788
6088
|
|
|
5789
6089
|
bubble.appendChild(textarea);
|
|
6090
|
+
bubble.appendChild(attachment);
|
|
5790
6091
|
bubble.appendChild(actions);
|
|
5791
6092
|
|
|
5792
6093
|
root.appendChild(bubble);
|
|
@@ -5895,6 +6196,7 @@ function initComty(options = {}) {
|
|
|
5895
6196
|
const item = document.createElement("button");
|
|
5896
6197
|
item.type = "button";
|
|
5897
6198
|
item.className = "comty-sidepanel-item";
|
|
6199
|
+
item.disabled = Boolean(comment.pending);
|
|
5898
6200
|
item.innerHTML = `
|
|
5899
6201
|
<span class="comty-sidepanel-item-status" aria-hidden="true"></span>
|
|
5900
6202
|
<span class="comty-sidepanel-item-body">
|
|
@@ -5915,7 +6217,7 @@ function initComty(options = {}) {
|
|
|
5915
6217
|
|
|
5916
6218
|
const metaEl = item.querySelector(".comty-sidepanel-item-meta");
|
|
5917
6219
|
if (metaEl instanceof HTMLElement) {
|
|
5918
|
-
metaEl.innerText = `${comment.author_name || "Anonymous"} · ${getCommentLabel(comment)}`;
|
|
6220
|
+
metaEl.innerText = `${comment.author_name || "Anonymous"} · ${comment.pending ? "Sending…" : getCommentLabel(comment)}`;
|
|
5919
6221
|
metaEl.title = getCommentLabel(comment);
|
|
5920
6222
|
}
|
|
5921
6223
|
|
|
@@ -6246,11 +6548,13 @@ function initComty(options = {}) {
|
|
|
6246
6548
|
const openCommentThread = createCommentThread({
|
|
6247
6549
|
root, apiUrl, projectKey, commentStatuses, selectedStatusFilters, placedPins,
|
|
6248
6550
|
closeBubbles, getCommentStatus, updateCommentStatus, applyStatusDot,
|
|
6249
|
-
renderSidepanelComments, applyStatusFilters, createThreadItem,
|
|
6551
|
+
renderSidepanelComments, applyStatusFilters, createThreadItem, renderCommentContent,
|
|
6250
6552
|
isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
|
|
6251
6553
|
updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
|
|
6252
6554
|
showAuthPrompt,
|
|
6253
6555
|
get canWrite() { return canWrite; },
|
|
6556
|
+
get canManage() { return canManage; },
|
|
6557
|
+
get allowGuestComments() { return allowGuestComments; },
|
|
6254
6558
|
get currentUser() { return currentUser; },
|
|
6255
6559
|
get cachedComments() { return cachedComments; },
|
|
6256
6560
|
set cachedComments(value) { cachedComments = value; },
|
|
@@ -6300,20 +6604,11 @@ function initComty(options = {}) {
|
|
|
6300
6604
|
const x = visibility.clientX;
|
|
6301
6605
|
const y = visibility.clientY;
|
|
6302
6606
|
|
|
6303
|
-
const pin =
|
|
6304
|
-
pin.
|
|
6305
|
-
pin.type = "button";
|
|
6607
|
+
const pin = createCommentPin(comment, x, y);
|
|
6608
|
+
pin.disabled = Boolean(comment.pending);
|
|
6306
6609
|
pin.dataset.status = getCommentStatus(comment.status).value;
|
|
6307
|
-
if (comment.id)
|
|
6308
|
-
|
|
6309
|
-
}
|
|
6310
|
-
pin.style.left = `${x}px`;
|
|
6311
|
-
pin.style.top = `${y}px`;
|
|
6312
|
-
setPinIcon(pin, comment);
|
|
6313
|
-
const pinStatusDot = document.createElement("span");
|
|
6314
|
-
pinStatusDot.className = "comty-pin-status-dot";
|
|
6315
|
-
applyStatusDot(pinStatusDot, comment.status);
|
|
6316
|
-
pin.appendChild(pinStatusDot);
|
|
6610
|
+
if (comment.id) pin.dataset.commentId = String(comment.id);
|
|
6611
|
+
const pinStatusDot = pin.querySelector(".comty-pin-status-dot");
|
|
6317
6612
|
pin.setAttribute(
|
|
6318
6613
|
"aria-label",
|
|
6319
6614
|
`Afficher le commentaire, statut ${getCommentStatus(comment.status).label}`,
|
|
@@ -6355,32 +6650,14 @@ function initComty(options = {}) {
|
|
|
6355
6650
|
activeHoverCommentIds.add(commentId);
|
|
6356
6651
|
}
|
|
6357
6652
|
|
|
6358
|
-
const hoverBubble =
|
|
6359
|
-
hoverBubble.className = "comty-bubble comty-hover-bubble";
|
|
6653
|
+
const hoverBubble = createCommentPreview(comment);
|
|
6360
6654
|
hoverBubble.style.left = `${x + 32}px`;
|
|
6361
6655
|
hoverBubble.style.top = `${y - 17}px`;
|
|
6362
|
-
hoverBubble.style.pointerEvents = "none";
|
|
6363
6656
|
hoverBubble.dataset.hoverId = commentId;
|
|
6364
6657
|
|
|
6365
|
-
const hoverThread = document.createElement("div");
|
|
6366
|
-
hoverThread.className = "comty-thread";
|
|
6367
|
-
const hoverItem = createThreadItem(comment, { showMore: false });
|
|
6368
|
-
hoverThread.appendChild(hoverItem.item);
|
|
6369
|
-
hoverBubble.appendChild(hoverThread);
|
|
6370
|
-
|
|
6371
6658
|
root.appendChild(hoverBubble);
|
|
6372
6659
|
|
|
6373
|
-
requestAnimationFrame(() =>
|
|
6374
|
-
if (x + 16 + hoverBubble.offsetWidth > window.innerWidth) {
|
|
6375
|
-
hoverBubble.style.left = `${x - hoverBubble.offsetWidth - 24}px`;
|
|
6376
|
-
hoverBubble.classList.add("comty-bubble-left");
|
|
6377
|
-
}
|
|
6378
|
-
|
|
6379
|
-
const rect = hoverBubble.getBoundingClientRect();
|
|
6380
|
-
if (rect.bottom > window.innerHeight) {
|
|
6381
|
-
hoverBubble.style.top = `${y - rect.height - 16}px`;
|
|
6382
|
-
}
|
|
6383
|
-
});
|
|
6660
|
+
requestAnimationFrame(() => positionCommentBubble(hoverBubble, x, y));
|
|
6384
6661
|
};
|
|
6385
6662
|
|
|
6386
6663
|
pin.onmouseleave = () => {
|
|
@@ -6477,6 +6754,9 @@ function initComty(options = {}) {
|
|
|
6477
6754
|
cachedComments = comments
|
|
6478
6755
|
.filter(comment => comment.kind !== "agent" && !comment.local_agent)
|
|
6479
6756
|
.map(comment => Object.assign(previous.get(String(comment.id)) || {}, comment));
|
|
6757
|
+
for (const comment of previous.values()) {
|
|
6758
|
+
if (comment.pending && !cachedComments.some(item => item.id === comment.id)) cachedComments.push(comment);
|
|
6759
|
+
}
|
|
6480
6760
|
}
|
|
6481
6761
|
|
|
6482
6762
|
async function loadComments({ force = false } = {}) {
|
|
@@ -6697,7 +6977,6 @@ function initComty(options = {}) {
|
|
|
6697
6977
|
hideInspector();
|
|
6698
6978
|
return;
|
|
6699
6979
|
}
|
|
6700
|
-
if (inspectedElement !== target) inspectExpanded = selectedElement ? inspectExpanded : false;
|
|
6701
6980
|
showInspector(target);
|
|
6702
6981
|
}, true);
|
|
6703
6982
|
document.documentElement.addEventListener("pointerleave", () => {
|
|
@@ -6754,7 +7033,6 @@ function initComty(options = {}) {
|
|
|
6754
7033
|
}
|
|
6755
7034
|
measuredRegion = null;
|
|
6756
7035
|
selectedElement = getAnchorElement(target);
|
|
6757
|
-
inspectExpanded = false;
|
|
6758
7036
|
showInspector(target);
|
|
6759
7037
|
return;
|
|
6760
7038
|
}
|
|
@@ -6912,13 +7190,26 @@ function initComty(options = {}) {
|
|
|
6912
7190
|
const data = await res.json();
|
|
6913
7191
|
projectId = data.projectId;
|
|
6914
7192
|
canWrite = Boolean(data.canWrite);
|
|
7193
|
+
canManage = Boolean(data.canManage ?? data.canWrite);
|
|
7194
|
+
allowGuestComments = Boolean(data.allowGuestComments);
|
|
6915
7195
|
currentUser = data.user || null;
|
|
6916
7196
|
loginUrl = data.loginUrl || "";
|
|
6917
7197
|
|
|
6918
7198
|
if (!canWrite && sdkToken) {
|
|
6919
|
-
sessionStorage.removeItem(tokenStorageKey);
|
|
7199
|
+
try { sessionStorage.removeItem(tokenStorageKey); } catch { /* Storage may be unavailable. */ }
|
|
6920
7200
|
sdkToken = "";
|
|
6921
7201
|
}
|
|
7202
|
+
if (didCompleteSignIn || authStatus === "forbidden") {
|
|
7203
|
+
let previousSession;
|
|
7204
|
+
try {
|
|
7205
|
+
previousSession = JSON.parse(sessionStorage.getItem(authReturnStorageKey) || "null");
|
|
7206
|
+
sessionStorage.removeItem(authReturnStorageKey);
|
|
7207
|
+
} catch { /* Fall back to the requested comment mode. */ }
|
|
7208
|
+
const mode = canWrite && ["browse", "edit", "inspect"].includes(previousSession?.mode)
|
|
7209
|
+
? previousSession.mode : canWrite ? "edit" : "browse";
|
|
7210
|
+
setSession(mode, { entry: previousSession?.entry || mode });
|
|
7211
|
+
if (previousSession?.sidepanelOpen) setSidepanelOpen(true);
|
|
7212
|
+
}
|
|
6922
7213
|
if (authStatus === "forbidden") showAuthPrompt(true);
|
|
6923
7214
|
if (canWrite && didCompleteSignIn) {
|
|
6924
7215
|
const toast = document.createElement("div");
|
|
@@ -6947,7 +7238,7 @@ function initComty(options = {}) {
|
|
|
6947
7238
|
setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
|
|
6948
7239
|
return renderComments(cachedComments, { retry: false });
|
|
6949
7240
|
},
|
|
6950
|
-
setCanWrite(value) { canWrite = value; },
|
|
7241
|
+
setCanWrite(value) { canWrite = value; canManage = value; },
|
|
6951
7242
|
invalidate() { anchorRevision += 1; },
|
|
6952
7243
|
});
|
|
6953
7244
|
}
|