@comty.io/core 0.1.3 → 0.1.5
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 +1233 -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 +251 -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,421 @@ 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 I(t){return Promise.resolve().then(()=>new XMLSerializer().serializeToString(t)).then(encodeURIComponent).then(e=>`data:image/svg+xml;charset=utf-8,${e}`)}async function U(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),I(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 V(t){let e=t.getPropertyValue("content");return`${t.cssText} content: '${e.replace(/'|"/g,"")}';`}function M(t,e){return y(e).map(r=>{let n=t.getPropertyValue(r),i=t.getPropertyPriority(r);return`${r}: ${n}${i?" !important":""};`}).join(" ")}function F(t,e,r,n){let i=`.${t}:${e}`,o=r.cssText?V(r):M(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(F(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",H={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 O(t){let e=/\.([^./]*?)$/g.exec(t);return e?e[1]:""}function m(t){let e=O(t).toLowerCase();return H[e]||""}function k(t){return t.split(/,/)[1]}function p(t){return t.search(/^(data:)/)!==-1}function x(t,e){return`data:${e};base64,${t}`}async function q(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 q(t,r.fetchRequestInit,({res:c,result:s})=>(e||(e=c.headers.get("Content-Type")||""),k(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 _(t){let e=t.toDataURL();return e==="data:,"?t.cloneNode(!1):w(e)}async function v(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 B(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 G(t,e){return l(t,HTMLCanvasElement)?_(t).catch(()=>t.cloneNode(!1)):l(t,HTMLVideoElement)?v(t,e).catch(()=>document.createElement("div")):l(t,HTMLIFrameElement)?B(t,e):t.cloneNode(!1)}var j=t=>t.tagName!=null&&t.tagName.toUpperCase()==="SLOT";async function z(t,e,r){var n,i;let o=[];return j(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 W(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 X(t,e){l(t,HTMLTextAreaElement)&&(e.textContent=t.value),l(t,HTMLInputElement)&&e.setAttribute("value",t.value)}function K(t,e){if(l(t,HTMLSelectElement)){let n=Array.from(e.children).find(i=>t.value===i.getAttribute("value"));n&&n.setAttribute("selected","")}}function J(t,e,r){return l(e,Element)&&(W(t,e,r),C(t,e,r),X(t,e),K(t,e)),e}async function Q(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=>G(i,e)).then(i=>z(t,i,e)).then(async i=>(await e.beforeClone?.(t),J(t,i,e))).then(i=>e.afterClone?.(t,i)??i).then(i=>Q(i,e))}var A=/url\((['"]?)([^'"]+?)\1\)/g,L=/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(A,(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(L.lastIndex=0;;){let[n,,i]=L.exec(r)||[];if(!i)return"";if(i===e)return`src: ${n};`}}):t}function rt(t){return t.search(A)!==-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=>D(i,e));await Promise.all(n).then(()=>t)}async function D(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:D,embedResources:E,nodeToDataURL:U}; })();
|
|
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
|
+
// Transparent page backgrounds reveal the browser canvas, not the image viewer.
|
|
2225
|
+
const viewport = document.createElement("div");
|
|
2226
|
+
viewport.style.backgroundColor = "Canvas";
|
|
2227
|
+
viewport.style.colorScheme = rootStyle.colorScheme;
|
|
2228
|
+
viewport.append(stage);
|
|
2229
|
+
const svg = await captureEngine.nodeToDataURL(viewport, context.width, context.height);
|
|
2230
|
+
const image = new Image();
|
|
2231
|
+
image.src = svg;
|
|
2232
|
+
await untilAborted(image.decode());
|
|
2233
|
+
checkContext();
|
|
2234
|
+
const scale = Math.min(2, devicePixelRatio || 1, 2560 / context.width, 2560 / context.height);
|
|
2235
|
+
const canvas = document.createElement("canvas");
|
|
2236
|
+
canvas.width = Math.round(context.width * scale);
|
|
2237
|
+
canvas.height = Math.round(context.height * scale);
|
|
2238
|
+
const ctx = canvas.getContext("2d");
|
|
2239
|
+
ctx.scale(scale, scale);
|
|
2240
|
+
ctx.drawImage(image, 0, 0);
|
|
2241
|
+
let blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/png")));
|
|
2242
|
+
if (!blob) throw new Error("Screenshot encoding failed");
|
|
2243
|
+
if (blob.size > 2800000) {
|
|
2244
|
+
blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", 0.9)));
|
|
2245
|
+
if (!blob) throw new Error("Screenshot encoding failed");
|
|
2246
|
+
}
|
|
2247
|
+
// CSS dimensions keep the eyedropper's coordinate system independent of DPR.
|
|
2248
|
+
return { blob, width: canvas.width, height: canvas.height, cssWidth: context.width, cssHeight: context.height };
|
|
2249
|
+
} finally {
|
|
2250
|
+
clearTimeout(timeout);
|
|
2251
|
+
controller.abort();
|
|
2152
2252
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2253
|
+
}
|
|
2254
|
+
return { captureViewportScreenshot };
|
|
2155
2255
|
}
|
|
2156
2256
|
|
|
2157
2257
|
// Page-local counterpart of the native eyedropper; never captures outside the page.
|
|
@@ -2199,7 +2299,9 @@ function createColorMagnifier({ root, isComtyElement, capture }) {
|
|
|
2199
2299
|
sampledHex = null;
|
|
2200
2300
|
if (!enabled) return;
|
|
2201
2301
|
try {
|
|
2202
|
-
const
|
|
2302
|
+
const screenshot = await capture();
|
|
2303
|
+
if (!screenshot) return;
|
|
2304
|
+
const { blob, cssWidth, cssHeight } = screenshot;
|
|
2203
2305
|
if (!enabled || version !== revision) return;
|
|
2204
2306
|
const dataUrl = await new Promise((resolve, reject) => {
|
|
2205
2307
|
const reader = new FileReader();
|
|
@@ -2215,7 +2317,11 @@ function createColorMagnifier({ root, isComtyElement, capture }) {
|
|
|
2215
2317
|
const probe = document.createElement('canvas').getContext('2d');
|
|
2216
2318
|
probe.drawImage(next, 0, 0);
|
|
2217
2319
|
probe.getImageData(0, 0, 1, 1);
|
|
2218
|
-
|
|
2320
|
+
const scaled = document.createElement('canvas');
|
|
2321
|
+
scaled.width = cssWidth || next.width;
|
|
2322
|
+
scaled.height = cssHeight || next.height;
|
|
2323
|
+
scaled.getContext('2d').drawImage(next, 0, 0, scaled.width, scaled.height);
|
|
2324
|
+
image = scaled;
|
|
2219
2325
|
paint();
|
|
2220
2326
|
} catch { /* The CSS-colour fallback remains available without screen permission. */ }
|
|
2221
2327
|
}
|
|
@@ -2262,7 +2368,7 @@ function createCommentThread(context) {
|
|
|
2262
2368
|
const {
|
|
2263
2369
|
root, apiUrl, projectKey, commentStatuses, selectedStatusFilters, placedPins,
|
|
2264
2370
|
closeBubbles, getCommentStatus, updateCommentStatus, applyStatusDot,
|
|
2265
|
-
renderSidepanelComments, applyStatusFilters, createThreadItem,
|
|
2371
|
+
renderSidepanelComments, applyStatusFilters, createThreadItem, renderCommentContent,
|
|
2266
2372
|
isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
|
|
2267
2373
|
updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
|
|
2268
2374
|
showAuthPrompt,
|
|
@@ -2290,14 +2396,6 @@ function createCommentThread(context) {
|
|
|
2290
2396
|
const header = document.createElement("div");
|
|
2291
2397
|
header.className = "comty-bubble-header";
|
|
2292
2398
|
|
|
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
2399
|
const statusMenuButton = document.createElement("button");
|
|
2302
2400
|
statusMenuButton.className = "comty-status-trigger";
|
|
2303
2401
|
statusMenuButton.type = "button";
|
|
@@ -2336,7 +2434,7 @@ function createCommentThread(context) {
|
|
|
2336
2434
|
};
|
|
2337
2435
|
|
|
2338
2436
|
const saveStatus = async (nextStatus, control) => {
|
|
2339
|
-
if (!context.
|
|
2437
|
+
if (!context.canManage) return false;
|
|
2340
2438
|
control.disabled = true;
|
|
2341
2439
|
const savedStatus = await updateCommentStatus(comment, nextStatus);
|
|
2342
2440
|
control.disabled = false;
|
|
@@ -2361,7 +2459,7 @@ function createCommentThread(context) {
|
|
|
2361
2459
|
return true;
|
|
2362
2460
|
};
|
|
2363
2461
|
|
|
2364
|
-
statusMenuButton.disabled = !context.
|
|
2462
|
+
statusMenuButton.disabled = !context.canManage;
|
|
2365
2463
|
statusMenuButton.onclick = (event) => {
|
|
2366
2464
|
event.stopPropagation();
|
|
2367
2465
|
const existingPopover = document.querySelector(
|
|
@@ -2373,10 +2471,11 @@ function createCommentThread(context) {
|
|
|
2373
2471
|
if (existingPopover) return;
|
|
2374
2472
|
|
|
2375
2473
|
const popover = document.createElement("div");
|
|
2376
|
-
popover.className = "comty-popover comty-status-popover";
|
|
2474
|
+
popover.className = "comty-popover comty-comment-popover comty-status-popover";
|
|
2377
2475
|
const buttonRect = statusMenuButton.getBoundingClientRect();
|
|
2378
2476
|
popover.style.top = `${buttonRect.bottom + 4}px`;
|
|
2379
|
-
popover.style.left = `${buttonRect.
|
|
2477
|
+
popover.style.left = `${buttonRect.left}px`;
|
|
2478
|
+
popover.style.transformOrigin = "top left";
|
|
2380
2479
|
|
|
2381
2480
|
for (const status of commentStatuses) {
|
|
2382
2481
|
const item = document.createElement("button");
|
|
@@ -2414,6 +2513,13 @@ function createCommentThread(context) {
|
|
|
2414
2513
|
}
|
|
2415
2514
|
|
|
2416
2515
|
root.appendChild(popover);
|
|
2516
|
+
const popoverWidth = popover.offsetWidth;
|
|
2517
|
+
const popoverHeight = popover.offsetHeight;
|
|
2518
|
+
popover.style.left = `${Math.max(8, Math.min(buttonRect.left, innerWidth - popoverWidth - 8))}px`;
|
|
2519
|
+
if (buttonRect.bottom + 4 + popoverHeight > innerHeight - 8) {
|
|
2520
|
+
popover.style.top = `${Math.max(8, buttonRect.top - popoverHeight - 4)}px`;
|
|
2521
|
+
popover.style.transformOrigin = "bottom left";
|
|
2522
|
+
}
|
|
2417
2523
|
|
|
2418
2524
|
const closeStatusPopover = (closeEvent) => {
|
|
2419
2525
|
if (
|
|
@@ -2430,9 +2536,7 @@ function createCommentThread(context) {
|
|
|
2430
2536
|
);
|
|
2431
2537
|
};
|
|
2432
2538
|
|
|
2433
|
-
|
|
2434
|
-
headerActions.appendChild(closeButton);
|
|
2435
|
-
header.appendChild(headerActions);
|
|
2539
|
+
header.append(statusMenuButton, closeButton);
|
|
2436
2540
|
bubble.appendChild(header);
|
|
2437
2541
|
|
|
2438
2542
|
const thread = document.createElement("div");
|
|
@@ -2451,7 +2555,7 @@ function createCommentThread(context) {
|
|
|
2451
2555
|
|
|
2452
2556
|
const popover = document.createElement("div");
|
|
2453
2557
|
popover.className =
|
|
2454
|
-
"comty-popover comty-entry-actions-popover";
|
|
2558
|
+
"comty-popover comty-comment-popover comty-entry-actions-popover";
|
|
2455
2559
|
|
|
2456
2560
|
const editItem = document.createElement("button");
|
|
2457
2561
|
editItem.className = "comty-popover-item";
|
|
@@ -2537,7 +2641,7 @@ function createCommentThread(context) {
|
|
|
2537
2641
|
|
|
2538
2642
|
const previousContent = entry.content;
|
|
2539
2643
|
entry.content = nextContent;
|
|
2540
|
-
item.content
|
|
2644
|
+
renderCommentContent(item.content, nextContent);
|
|
2541
2645
|
editor.disabled = true;
|
|
2542
2646
|
editor.replaceWith(item.content);
|
|
2543
2647
|
renderSidepanelComments();
|
|
@@ -2545,11 +2649,11 @@ function createCommentThread(context) {
|
|
|
2545
2649
|
const savedContent = await persist(nextContent);
|
|
2546
2650
|
if (!savedContent) {
|
|
2547
2651
|
entry.content = previousContent;
|
|
2548
|
-
item.content
|
|
2652
|
+
renderCommentContent(item.content, previousContent || "");
|
|
2549
2653
|
renderSidepanelComments();
|
|
2550
2654
|
return;
|
|
2551
2655
|
}
|
|
2552
|
-
item.content
|
|
2656
|
+
renderCommentContent(item.content, savedContent);
|
|
2553
2657
|
renderSidepanelComments();
|
|
2554
2658
|
};
|
|
2555
2659
|
|
|
@@ -2655,9 +2759,11 @@ function createCommentThread(context) {
|
|
|
2655
2759
|
replyForm.appendChild(createCommentAvatar(context.currentUser || { name: "You" }));
|
|
2656
2760
|
const replyField = document.createElement("div");
|
|
2657
2761
|
replyField.className = "comty-reply-field";
|
|
2658
|
-
const replyInput = document.createElement("
|
|
2762
|
+
const replyInput = document.createElement("textarea");
|
|
2659
2763
|
replyInput.className = "comty-reply-input";
|
|
2660
2764
|
replyInput.placeholder = "Reply";
|
|
2765
|
+
replyInput.rows = 1;
|
|
2766
|
+
replyInput.setAttribute("aria-label", "Reply");
|
|
2661
2767
|
|
|
2662
2768
|
const replyButton = document.createElement("button");
|
|
2663
2769
|
replyButton.className = "comty-reply-button";
|
|
@@ -2691,6 +2797,7 @@ function createCommentThread(context) {
|
|
|
2691
2797
|
comment.replies = replies;
|
|
2692
2798
|
appendReply(data.reply);
|
|
2693
2799
|
replyInput.value = "";
|
|
2800
|
+
syncReplySize();
|
|
2694
2801
|
requestAnimationFrame(() => {
|
|
2695
2802
|
threadList.scrollTop = threadList.scrollHeight;
|
|
2696
2803
|
});
|
|
@@ -2704,12 +2811,28 @@ function createCommentThread(context) {
|
|
|
2704
2811
|
}
|
|
2705
2812
|
};
|
|
2706
2813
|
|
|
2814
|
+
const syncReplySize = () => {
|
|
2815
|
+
const currentHeight = replyInput.getBoundingClientRect().height;
|
|
2816
|
+
replyInput.style.transition = "none";
|
|
2817
|
+
const lineHeight = parseFloat(getComputedStyle(replyInput).lineHeight);
|
|
2818
|
+
replyInput.style.height = `${lineHeight}px`;
|
|
2819
|
+
const contentHeight = replyInput.scrollHeight;
|
|
2820
|
+
replyInput.style.height = `${currentHeight}px`;
|
|
2821
|
+
replyInput.getBoundingClientRect();
|
|
2822
|
+
replyInput.style.transition = "";
|
|
2823
|
+
const nextHeight = Math.min(contentHeight, parseFloat(getComputedStyle(replyInput).maxHeight));
|
|
2824
|
+
replyInput.style.height = `${nextHeight}px`;
|
|
2825
|
+
if (contentHeight <= lineHeight + 1) replyInput.scrollTop = 0;
|
|
2826
|
+
replyField.classList.toggle("comty-reply-expanded", contentHeight > lineHeight + 1);
|
|
2827
|
+
};
|
|
2828
|
+
|
|
2707
2829
|
replyInput.oninput = () => {
|
|
2830
|
+
syncReplySize();
|
|
2708
2831
|
replyButton.disabled = replyInput.value.trim().length === 0;
|
|
2709
2832
|
};
|
|
2710
2833
|
replyInput.onkeydown = (event) => {
|
|
2711
2834
|
event.stopPropagation();
|
|
2712
|
-
if (event.key === "Enter" && !event.shiftKey) {
|
|
2835
|
+
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
|
|
2713
2836
|
event.preventDefault();
|
|
2714
2837
|
void submitReply();
|
|
2715
2838
|
}
|
|
@@ -2727,10 +2850,10 @@ function createCommentThread(context) {
|
|
|
2727
2850
|
const signInButton = document.createElement("button");
|
|
2728
2851
|
signInButton.className = "comty-sign-in-reply";
|
|
2729
2852
|
signInButton.type = "button";
|
|
2730
|
-
signInButton.innerText = "Sign in to reply";
|
|
2853
|
+
signInButton.innerText = context.allowGuestComments ? "Leave a reply" : "Sign in to reply";
|
|
2731
2854
|
signInButton.onclick = (event) => {
|
|
2732
2855
|
event.stopPropagation();
|
|
2733
|
-
showAuthPrompt();
|
|
2856
|
+
showAuthPrompt(false, () => openCommentThread(comment, pin, pinStatusDot, x, y, removeHoverBubble));
|
|
2734
2857
|
};
|
|
2735
2858
|
replyForm.appendChild(signInButton);
|
|
2736
2859
|
}
|
|
@@ -3020,11 +3143,14 @@ function initComty(options = {}) {
|
|
|
3020
3143
|
const observedCommentTargets = new WeakSet();
|
|
3021
3144
|
let sidepanelOpen = false;
|
|
3022
3145
|
let canWrite = false;
|
|
3146
|
+
let canManage = false;
|
|
3147
|
+
let allowGuestComments = false;
|
|
3023
3148
|
let loginUrl = "";
|
|
3024
3149
|
let currentUser = null;
|
|
3025
3150
|
let authStatus = "";
|
|
3026
3151
|
let didCompleteSignIn = false;
|
|
3027
3152
|
const tokenStorageKey = `comty:${projectKey}:token`;
|
|
3153
|
+
const authReturnStorageKey = `comty:${projectKey}:auth-return`;
|
|
3028
3154
|
let sdkToken = "";
|
|
3029
3155
|
const activeHoverCommentIds = new Set();
|
|
3030
3156
|
/** @type {null | 'edit' | 'browse'} */
|
|
@@ -3033,7 +3159,6 @@ function initComty(options = {}) {
|
|
|
3033
3159
|
const placementDiagnostics = new Map();
|
|
3034
3160
|
let inspectedElement = null;
|
|
3035
3161
|
let selectedElement = null;
|
|
3036
|
-
let inspectExpanded = false;
|
|
3037
3162
|
let measureAlt = false;
|
|
3038
3163
|
let inspectDrag = null;
|
|
3039
3164
|
let measuredRegion = null;
|
|
@@ -3715,6 +3840,21 @@ function initComty(options = {}) {
|
|
|
3715
3840
|
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
3841
|
const checkIcon = icon('<path d="m5 12 4 4 10-10"/>');
|
|
3717
3842
|
const copyFeedbackTimers = new WeakMap();
|
|
3843
|
+
async function copyPropertyValue(button, feedback) {
|
|
3844
|
+
clearTimeout(copyFeedbackTimers.get(button));
|
|
3845
|
+
try {
|
|
3846
|
+
await navigator.clipboard.writeText(button.dataset.copy);
|
|
3847
|
+
if (!button.isConnected) return;
|
|
3848
|
+
button.dataset.copied = "";
|
|
3849
|
+
feedback.textContent = "Copied";
|
|
3850
|
+
clearTimeout(copyFeedbackTimers.get(button));
|
|
3851
|
+
copyFeedbackTimers.set(button, setTimeout(() => { delete button.dataset.copied; }, 1200));
|
|
3852
|
+
} catch {
|
|
3853
|
+
delete button.dataset.copied;
|
|
3854
|
+
if (button.isConnected) feedback.textContent = "Clipboard unavailable — select the value to copy";
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3718
3858
|
function propertyRow(label, value, display = value) {
|
|
3719
3859
|
const unavailable = value === "—";
|
|
3720
3860
|
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 +3881,7 @@ function initComty(options = {}) {
|
|
|
3741
3881
|
propertyPanel.setAttribute("aria-label", propertyKind === "color" ? "Recent colours" : "Recent typography");
|
|
3742
3882
|
const recent = propertyKind === "color" ? recentColors : recentFonts;
|
|
3743
3883
|
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 ?
|
|
3884
|
+
? `<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
3885
|
: propertyKind === "font" && expanded
|
|
3746
3886
|
? `<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
3887
|
: "";
|
|
@@ -3857,18 +3997,7 @@ function initComty(options = {}) {
|
|
|
3857
3997
|
renderPropertyPanel(true);
|
|
3858
3998
|
propertyPanel.querySelector(`button[data-recent="${button.dataset.recent}"]`)?.focus({ preventScroll: true });
|
|
3859
3999
|
} 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
|
-
}
|
|
4000
|
+
await copyPropertyValue(button, propertyPanel.querySelector(".comty-property-feedback"));
|
|
3872
4001
|
}
|
|
3873
4002
|
};
|
|
3874
4003
|
|
|
@@ -4365,7 +4494,8 @@ function initComty(options = {}) {
|
|
|
4365
4494
|
: headers;
|
|
4366
4495
|
}
|
|
4367
4496
|
|
|
4368
|
-
function showAuthPrompt(forbidden = false) {
|
|
4497
|
+
function showAuthPrompt(forbidden = false, onReady = startEditSession) {
|
|
4498
|
+
const guestAllowed = allowGuestComments;
|
|
4369
4499
|
document.querySelector(".comty-auth-backdrop")?.remove();
|
|
4370
4500
|
const opener = document.activeElement;
|
|
4371
4501
|
const backdrop = document.createElement("div");
|
|
@@ -4376,12 +4506,23 @@ function initComty(options = {}) {
|
|
|
4376
4506
|
<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
4507
|
<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
4508
|
</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
|
-
|
|
4509
|
+
<h2 id="comty-auth-title">${guestAllowed ? "Leave a comment" : forbidden ? "Project access required" : "Sign in to comment"}</h2>
|
|
4510
|
+
<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>
|
|
4511
|
+
${guestAllowed ? `
|
|
4512
|
+
<form class="comty-guest-form">
|
|
4513
|
+
<label for="comty-guest-name">Your name</label>
|
|
4514
|
+
<input id="comty-guest-name" name="name" autocomplete="name" placeholder="Your name" maxlength="80" required aria-describedby="comty-guest-error" />
|
|
4515
|
+
<p id="comty-guest-error" role="alert" hidden></p>
|
|
4516
|
+
<div class="comty-auth-actions comty-guest-actions">
|
|
4517
|
+
<button type="submit" class="comty-auth-primary" data-comty-guest>Continue</button>
|
|
4518
|
+
<span class="comty-auth-or">or</span>
|
|
4519
|
+
<button type="button" data-comty-login>Continue with Google</button>
|
|
4520
|
+
</div>
|
|
4521
|
+
</form>` : `
|
|
4522
|
+
<div class="comty-auth-actions">
|
|
4523
|
+
<button type="button" data-comty-close>${forbidden ? "Close" : "Cancel"}</button>
|
|
4524
|
+
${forbidden ? "" : '<button type="button" class="comty-auth-primary" data-comty-login>Continue to sign in</button>'}
|
|
4525
|
+
</div>`}
|
|
4385
4526
|
</div>`;
|
|
4386
4527
|
const close = () => {
|
|
4387
4528
|
backdrop.remove();
|
|
@@ -4392,11 +4533,12 @@ function initComty(options = {}) {
|
|
|
4392
4533
|
event.stopPropagation();
|
|
4393
4534
|
if (event.key === "Escape") { event.preventDefault(); close(); }
|
|
4394
4535
|
if (event.key === "Tab") {
|
|
4395
|
-
const buttons = [...backdrop.querySelectorAll("button:not(:disabled)")];
|
|
4536
|
+
const buttons = [...backdrop.querySelectorAll("button:not(:disabled), input:not(:disabled)")];
|
|
4396
4537
|
if (!buttons.length) { event.preventDefault(); return; }
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4538
|
+
event.preventDefault();
|
|
4539
|
+
const index = buttons.indexOf(document.activeElement);
|
|
4540
|
+
const next = (index + (event.shiftKey ? -1 : 1) + buttons.length) % buttons.length;
|
|
4541
|
+
buttons[next].focus();
|
|
4400
4542
|
}
|
|
4401
4543
|
});
|
|
4402
4544
|
const loginButton = backdrop.querySelector("[data-comty-login]");
|
|
@@ -4404,15 +4546,62 @@ function initComty(options = {}) {
|
|
|
4404
4546
|
loginButton.onclick = () => {
|
|
4405
4547
|
loginButton.disabled = true;
|
|
4406
4548
|
loginButton.innerText = "Connecting…";
|
|
4407
|
-
backdrop.querySelectorAll("
|
|
4408
|
-
|
|
4549
|
+
backdrop.querySelectorAll("button, input").forEach(control => { control.disabled = true; });
|
|
4550
|
+
try {
|
|
4551
|
+
sessionStorage.setItem(authReturnStorageKey, JSON.stringify({
|
|
4552
|
+
mode: onReady === startEditSession || session.mode === "off" ? "edit" : session.mode,
|
|
4553
|
+
entry: sessionEntry,
|
|
4554
|
+
sidepanelOpen,
|
|
4555
|
+
}));
|
|
4556
|
+
} catch { /* Resume in comment mode if browser storage is unavailable. */ }
|
|
4557
|
+
const destination = new URL("/api/auth/sign-in/google", apiUrl);
|
|
4558
|
+
destination.searchParams.set("callbackURL", loginUrl);
|
|
4559
|
+
setTimeout(() => window.location.assign(guestAllowed ? destination.toString() : loginUrl), 50);
|
|
4409
4560
|
};
|
|
4410
4561
|
}
|
|
4562
|
+
const guestForm = backdrop.querySelector(".comty-guest-form");
|
|
4563
|
+
if (guestForm) guestForm.onsubmit = async event => {
|
|
4564
|
+
event.preventDefault();
|
|
4565
|
+
const nameInput = guestForm.querySelector("input");
|
|
4566
|
+
const error = guestForm.querySelector("[role=alert]");
|
|
4567
|
+
const name = nameInput.value.trim();
|
|
4568
|
+
if (!name) { nameInput.focus(); return; }
|
|
4569
|
+
const controls = [...backdrop.querySelectorAll("button, input")];
|
|
4570
|
+
controls.forEach(control => { control.disabled = true; });
|
|
4571
|
+
error.hidden = true;
|
|
4572
|
+
const continueButton = guestForm.querySelector("[data-comty-guest]");
|
|
4573
|
+
continueButton.textContent = "Connecting…";
|
|
4574
|
+
try {
|
|
4575
|
+
const response = await fetch(`${apiUrl}/api/sdk/guest`, {
|
|
4576
|
+
method: "POST",
|
|
4577
|
+
headers: { "Content-Type": "application/json" },
|
|
4578
|
+
body: JSON.stringify({ projectKey, name }),
|
|
4579
|
+
});
|
|
4580
|
+
const data = await response.json();
|
|
4581
|
+
if (!response.ok) throw new Error(data.error || "Unable to continue. Please try again.");
|
|
4582
|
+
sdkToken = data.token;
|
|
4583
|
+
try { sessionStorage.setItem(tokenStorageKey, sdkToken); } catch { /* Keep the session in memory when browser storage is unavailable. */ }
|
|
4584
|
+
currentUser = data.user;
|
|
4585
|
+
canWrite = true;
|
|
4586
|
+
canManage = false;
|
|
4587
|
+
if (backdrop.isConnected) {
|
|
4588
|
+
close();
|
|
4589
|
+
onReady();
|
|
4590
|
+
}
|
|
4591
|
+
} catch (failure) {
|
|
4592
|
+
if (!backdrop.isConnected) return;
|
|
4593
|
+
error.textContent = failure.message || "Unable to connect. Please try again.";
|
|
4594
|
+
error.hidden = false;
|
|
4595
|
+
controls.forEach(control => { control.disabled = false; });
|
|
4596
|
+
continueButton.textContent = "Continue";
|
|
4597
|
+
nameInput.focus();
|
|
4598
|
+
}
|
|
4599
|
+
};
|
|
4411
4600
|
backdrop.onclick = (event) => {
|
|
4412
4601
|
if (event.target === backdrop) close();
|
|
4413
4602
|
};
|
|
4414
4603
|
root.appendChild(backdrop);
|
|
4415
|
-
(loginButton || backdrop.querySelector("[data-comty-close]")).focus({ preventScroll: true });
|
|
4604
|
+
(backdrop.querySelector("#comty-guest-name") || loginButton || backdrop.querySelector("[data-comty-close]")).focus({ preventScroll: true });
|
|
4416
4605
|
}
|
|
4417
4606
|
|
|
4418
4607
|
function formatBoxValue(value) {
|
|
@@ -4609,7 +4798,8 @@ function initComty(options = {}) {
|
|
|
4609
4798
|
add("background", css.backgroundColor, (tokens.background.length > 0 || !["rgba(0, 0, 0, 0)", "rgb(255, 255, 255)"].includes(css.backgroundColor)));
|
|
4610
4799
|
add("color", css.color);
|
|
4611
4800
|
if (getElementOwnTextSignature(el)) {
|
|
4612
|
-
add("font",
|
|
4801
|
+
add("font-size", css.fontSize);
|
|
4802
|
+
add("font-weight", css.fontWeight);
|
|
4613
4803
|
add("font-family", css.fontFamily);
|
|
4614
4804
|
add("line-height", css.lineHeight);
|
|
4615
4805
|
add("letter-spacing", css.letterSpacing, css.letterSpacing !== "normal");
|
|
@@ -4793,7 +4983,7 @@ function initComty(options = {}) {
|
|
|
4793
4983
|
height: draftBubble.offsetHeight,
|
|
4794
4984
|
};
|
|
4795
4985
|
const pinBounds = draftPin?.getBoundingClientRect();
|
|
4796
|
-
const signature = JSON.stringify([selector, rect, hoverRect, rows, !!selected,
|
|
4986
|
+
const signature = JSON.stringify([selector, rect, hoverRect, rows, !!selected, measureAlt, draftBounds, pinBounds, spacing, innerWidth, innerHeight]);
|
|
4797
4987
|
if (signature === inspectionSignature) return;
|
|
4798
4988
|
inspectionSignature = signature;
|
|
4799
4989
|
inspectionLayer.replaceChildren();
|
|
@@ -4819,36 +5009,34 @@ function initComty(options = {}) {
|
|
|
4819
5009
|
const panel = document.createElement("div");
|
|
4820
5010
|
panel.className = "comty-inspector";
|
|
4821
5011
|
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
|
-
|
|
5012
|
+
if (selected || draftPreview) panel.dataset.selected = "";
|
|
5013
|
+
const priority = ["background", "color", "font-family", "font-size", "font-weight", "margin", "line-height", "letter-spacing"];
|
|
5014
|
+
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))];
|
|
5015
|
+
const labels = { "font-family": "Font", "font-size": "Size", "font-weight": "Weight", "line-height": "Line height", "letter-spacing": "Spacing", "background": "Background", "color": "Color", "margin": "Margin" };
|
|
5016
|
+
const cssProperty = key => ({ radius: "border-radius", background: "background-color" })[key] || key;
|
|
5017
|
+
const copyValue = (value, tokens) => tokens?.length ? `var(${tokens[0]})` : value;
|
|
5018
|
+
const allProperties = `${selector} {\n${rows.map(([key, value, tokens]) => ` ${cssProperty(key)}: ${copyValue(value, tokens)};`).join("\n")}\n}`;
|
|
5019
|
+
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>`;
|
|
5020
|
+
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]) => {
|
|
5021
|
+
const label = labels[key] || key.replaceAll("-", " ").replace(/^./, c => c.toUpperCase());
|
|
5022
|
+
const isColor = key === "background" || key === "color";
|
|
5023
|
+
const picker = isColor ? "color" : key.startsWith("font-") || ["line-height", "letter-spacing"].includes(key) ? "font" : "";
|
|
5024
|
+
const displayValue = isColor ? colorDetails(value)?.hex || value : value;
|
|
5025
|
+
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>`;
|
|
5026
|
+
const valueElement = picker && selected
|
|
5027
|
+
? `<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>`
|
|
5028
|
+
: `<span class="comty-inspector-value" data-property="${key}">${valueMarkup}</span>`;
|
|
5029
|
+
return `<div class="comty-inspector-row"><span class="comty-inspector-label">${label}</span>${valueElement}${copyButton(tokens?.length ? tokens[0].replace(/^--/, "") : value, `Copy ${label}`)}</div>`;
|
|
5030
|
+
}).join("")}</div><span class="comty-property-feedback" role="status"></span>`;
|
|
5031
|
+
for (const swatch of panel.querySelectorAll('.comty-inspector-swatch')) {
|
|
5032
|
+
const key = swatch.parentElement.dataset.property;
|
|
5033
|
+
swatch.style.backgroundColor = rows.find(([name]) => name === key)?.[1];
|
|
4835
5034
|
}
|
|
4836
5035
|
panel.onclick = async (event) => {
|
|
4837
5036
|
const button = event.target.closest("button");
|
|
4838
5037
|
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
|
-
}
|
|
5038
|
+
if (button.dataset.picker) openPropertyPanel(button.dataset.picker, target, button.dataset.property, button);
|
|
5039
|
+
else await copyPropertyValue(button, panel.querySelector(".comty-property-feedback"));
|
|
4852
5040
|
};
|
|
4853
5041
|
inspectionLayer.appendChild(panel);
|
|
4854
5042
|
const bounds = panel.getBoundingClientRect();
|
|
@@ -4866,8 +5054,24 @@ function initComty(options = {}) {
|
|
|
4866
5054
|
}
|
|
4867
5055
|
return;
|
|
4868
5056
|
}
|
|
4869
|
-
|
|
4870
|
-
|
|
5057
|
+
const inset = 8, gap = 12;
|
|
5058
|
+
const clampX = x => Math.max(inset, Math.min(x, innerWidth - bounds.width - inset));
|
|
5059
|
+
const clampY = y => Math.max(inset, Math.min(y, innerHeight - bounds.height - inset));
|
|
5060
|
+
const x = clampX(rect.left), y = clampY(rect.top);
|
|
5061
|
+
const candidates = [
|
|
5062
|
+
{ left: x, top: rect.bottom + gap },
|
|
5063
|
+
{ left: rect.right + gap, top: y },
|
|
5064
|
+
{ left: rect.left - bounds.width - gap, top: y },
|
|
5065
|
+
{ left: x, top: rect.top - bounds.height - gap },
|
|
5066
|
+
];
|
|
5067
|
+
let position = candidates.find(({ left, top }) => left >= inset && top >= inset && left + bounds.width <= innerWidth - inset && top + bounds.height <= innerHeight - inset);
|
|
5068
|
+
if (!position) {
|
|
5069
|
+
// A viewport-sized target leaves no outside space; minimize the covered area.
|
|
5070
|
+
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));
|
|
5071
|
+
position = candidates.map(({ left, top }) => ({ left: clampX(left), top: clampY(top) })).sort((a, b) => overlap(a) - overlap(b))[0];
|
|
5072
|
+
}
|
|
5073
|
+
panel.style.left = `${position.left}px`;
|
|
5074
|
+
panel.style.top = `${position.top}px`;
|
|
4871
5075
|
}
|
|
4872
5076
|
|
|
4873
5077
|
function endSession() {
|
|
@@ -5256,15 +5460,6 @@ function initComty(options = {}) {
|
|
|
5256
5460
|
return element;
|
|
5257
5461
|
}
|
|
5258
5462
|
|
|
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
5463
|
function getDeviceContext() {
|
|
5269
5464
|
const width = window.innerWidth;
|
|
5270
5465
|
const height = window.innerHeight;
|
|
@@ -5339,125 +5534,44 @@ function initComty(options = {}) {
|
|
|
5339
5534
|
if (!session.edit || hadDraft) hideInspector();
|
|
5340
5535
|
document
|
|
5341
5536
|
.querySelectorAll(
|
|
5342
|
-
".comty-bubble, .comty-create-bubble, .comty-temp-pin, .comty-popover",
|
|
5537
|
+
".comty-bubble:not([data-comty-capture-keep]), .comty-create-bubble, .comty-temp-pin, .comty-popover",
|
|
5343
5538
|
)
|
|
5344
5539
|
.forEach((x) => x.remove());
|
|
5345
5540
|
}
|
|
5346
5541
|
|
|
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
|
-
}
|
|
5542
|
+
const { captureViewportScreenshot } = createScreenshotCapture({ apiUrl, projectKey, getHeaders: getSdkHeaders });
|
|
5383
5543
|
|
|
5384
|
-
async function uploadCommentScreenshot(
|
|
5385
|
-
commentId,
|
|
5386
|
-
payload,
|
|
5387
|
-
captureNodes = [],
|
|
5388
|
-
) {
|
|
5544
|
+
async function uploadCommentScreenshot(comment, context, point) {
|
|
5389
5545
|
try {
|
|
5390
|
-
const
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
if (!formData.has("screenshot")) return;
|
|
5399
|
-
|
|
5546
|
+
const screenshot = await captureViewportScreenshot({ context, annotation: () => mountCaptureAnnotation(comment, point) });
|
|
5547
|
+
if (!screenshot) return;
|
|
5548
|
+
const formData = new FormData();
|
|
5549
|
+
formData.append("projectKey", projectKey);
|
|
5550
|
+
formData.append("screenshot", screenshot.blob, screenshot.blob.type === "image/jpeg" ? "comment.jpg" : "comment.png");
|
|
5551
|
+
formData.append("screenshotWidth", String(screenshot.width));
|
|
5552
|
+
formData.append("screenshotHeight", String(screenshot.height));
|
|
5400
5553
|
const response = await fetch(
|
|
5401
|
-
`${apiUrl}/api/comments/${encodeURIComponent(
|
|
5402
|
-
{
|
|
5403
|
-
method: "POST",
|
|
5404
|
-
headers: getSdkHeaders(),
|
|
5405
|
-
body: formData,
|
|
5406
|
-
},
|
|
5554
|
+
`${apiUrl}/api/comments/${encodeURIComponent(comment.id)}/screenshot`,
|
|
5555
|
+
{ method: "POST", headers: getSdkHeaders(), body: formData },
|
|
5407
5556
|
);
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
await loadComments({ force: true });
|
|
5557
|
+
if (!response.ok) throw new Error(`Screenshot upload failed (${response.status})`);
|
|
5558
|
+
// Updating metadata must not redraw the page or interrupt a new draft.
|
|
5559
|
+
const { screenshotUrl } = await response.json();
|
|
5560
|
+
const cached = cachedComments.find(item => String(item.id) === String(comment.id));
|
|
5561
|
+
if (cached) Object.assign(cached, {
|
|
5562
|
+
screenshot_url: screenshotUrl,
|
|
5563
|
+
screenshot_width: screenshot.width,
|
|
5564
|
+
screenshot_height: screenshot.height,
|
|
5565
|
+
});
|
|
5418
5566
|
} 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];
|
|
5567
|
+
console.warn("Comty: comment saved, screenshot unavailable", error);
|
|
5568
|
+
}
|
|
5456
5569
|
}
|
|
5457
5570
|
|
|
5458
5571
|
function addLocalComment(commentId, payload) {
|
|
5459
5572
|
const comment = {
|
|
5460
5573
|
id: commentId,
|
|
5574
|
+
pending: commentId.startsWith("pending-"),
|
|
5461
5575
|
project_id: projectId,
|
|
5462
5576
|
domain: payload.host || window.location.host,
|
|
5463
5577
|
page_path: payload.pagePath || window.location.pathname,
|
|
@@ -5465,6 +5579,7 @@ function initComty(options = {}) {
|
|
|
5465
5579
|
rel_x: payload.relX,
|
|
5466
5580
|
rel_y: payload.relY,
|
|
5467
5581
|
content: payload.content || "",
|
|
5582
|
+
link_url: payload.linkUrl || null,
|
|
5468
5583
|
created_at: new Date().toISOString(),
|
|
5469
5584
|
dom_element: payload.domElement || null,
|
|
5470
5585
|
viewport: payload.viewport || null,
|
|
@@ -5476,7 +5591,9 @@ function initComty(options = {}) {
|
|
|
5476
5591
|
screenshot_width: null,
|
|
5477
5592
|
screenshot_height: null,
|
|
5478
5593
|
anchor_context: payload.anchorContext || null,
|
|
5479
|
-
author_user_id: currentUser?.id || null,
|
|
5594
|
+
author_user_id: currentUser?.isGuest ? null : currentUser?.id || null,
|
|
5595
|
+
guest_author_id: currentUser?.isGuest ? currentUser.id : null,
|
|
5596
|
+
guest_author_name: currentUser?.isGuest ? currentUser.name : null,
|
|
5480
5597
|
author_name: currentUser?.name || null,
|
|
5481
5598
|
author_avatar_url: currentUser?.avatarUrl || null,
|
|
5482
5599
|
replies: [],
|
|
@@ -5491,6 +5608,7 @@ function initComty(options = {}) {
|
|
|
5491
5608
|
];
|
|
5492
5609
|
renderSidepanelComments();
|
|
5493
5610
|
void renderComments(cachedComments, { retry: false });
|
|
5611
|
+
return comment;
|
|
5494
5612
|
}
|
|
5495
5613
|
|
|
5496
5614
|
function getCommentAuthorName(comment) {
|
|
@@ -5500,8 +5618,7 @@ function initComty(options = {}) {
|
|
|
5500
5618
|
function isOwnCommentEntry(entry) {
|
|
5501
5619
|
return Boolean(
|
|
5502
5620
|
currentUser?.id &&
|
|
5503
|
-
entry?.author_user_id
|
|
5504
|
-
entry.author_user_id === currentUser.id,
|
|
5621
|
+
(currentUser.isGuest ? entry?.guest_author_id : entry?.author_user_id) === currentUser.id,
|
|
5505
5622
|
);
|
|
5506
5623
|
}
|
|
5507
5624
|
|
|
@@ -5577,7 +5694,7 @@ function initComty(options = {}) {
|
|
|
5577
5694
|
|
|
5578
5695
|
function createThreadItem(
|
|
5579
5696
|
entry,
|
|
5580
|
-
{ showMore = isOwnCommentEntry(entry) } = {},
|
|
5697
|
+
{ showMore = isOwnCommentEntry(entry), showLink = true } = {},
|
|
5581
5698
|
) {
|
|
5582
5699
|
const item = document.createElement("div");
|
|
5583
5700
|
item.className = "comty-thread-item";
|
|
@@ -5612,15 +5729,51 @@ function initComty(options = {}) {
|
|
|
5612
5729
|
|
|
5613
5730
|
const content = document.createElement("div");
|
|
5614
5731
|
content.className = "comty-bubble-content";
|
|
5615
|
-
content
|
|
5732
|
+
renderCommentContent(content, entry?.content || "");
|
|
5616
5733
|
|
|
5617
5734
|
body.appendChild(author);
|
|
5618
5735
|
body.appendChild(content);
|
|
5736
|
+
if (showLink && entry?.link_url) body.appendChild(createCommentLink(entry.link_url));
|
|
5619
5737
|
item.appendChild(body);
|
|
5620
5738
|
|
|
5621
5739
|
return { item, body, content, moreButton };
|
|
5622
5740
|
}
|
|
5623
5741
|
|
|
5742
|
+
function renderCommentContent(element, value) {
|
|
5743
|
+
const content = String(value ?? "");
|
|
5744
|
+
const nodes = [];
|
|
5745
|
+
let previousEnd = 0;
|
|
5746
|
+
for (const match of content.matchAll(/https?:\/\/[^\s<>"']+/gi)) {
|
|
5747
|
+
const url = match[0].replace(/[.,!?;:]+$/, "");
|
|
5748
|
+
if (!url) continue;
|
|
5749
|
+
let parsed;
|
|
5750
|
+
try { parsed = new URL(url); } catch { continue; }
|
|
5751
|
+
if (!["http:", "https:"].includes(parsed.protocol) || !parsed.hostname) continue;
|
|
5752
|
+
nodes.push(document.createTextNode(content.slice(previousEnd, match.index)));
|
|
5753
|
+
const link = document.createElement("a");
|
|
5754
|
+
link.href = url;
|
|
5755
|
+
link.target = "_blank";
|
|
5756
|
+
link.rel = "noopener noreferrer";
|
|
5757
|
+
link.textContent = url;
|
|
5758
|
+
nodes.push(link);
|
|
5759
|
+
previousEnd = match.index + url.length;
|
|
5760
|
+
}
|
|
5761
|
+
nodes.push(document.createTextNode(content.slice(previousEnd)));
|
|
5762
|
+
element.replaceChildren(...nodes);
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5765
|
+
function createCommentLink(url) {
|
|
5766
|
+
const link = document.createElement("a");
|
|
5767
|
+
link.className = "comty-comment-link";
|
|
5768
|
+
link.href = url;
|
|
5769
|
+
link.target = "_blank";
|
|
5770
|
+
link.rel = "noopener noreferrer";
|
|
5771
|
+
link.title = url;
|
|
5772
|
+
link.setAttribute("aria-label", `Open attached link: ${url}`);
|
|
5773
|
+
link.textContent = url;
|
|
5774
|
+
return link;
|
|
5775
|
+
}
|
|
5776
|
+
|
|
5624
5777
|
function setPinIcon(pin, comment = null) {
|
|
5625
5778
|
if (comment) {
|
|
5626
5779
|
pin.classList.add("comty-pin-with-avatar");
|
|
@@ -5637,6 +5790,54 @@ function initComty(options = {}) {
|
|
|
5637
5790
|
`;
|
|
5638
5791
|
}
|
|
5639
5792
|
|
|
5793
|
+
function createCommentPin(comment, x, y) {
|
|
5794
|
+
const pin = document.createElement("button");
|
|
5795
|
+
pin.className = "comty-pin";
|
|
5796
|
+
pin.type = "button";
|
|
5797
|
+
pin.style.left = `${x}px`;
|
|
5798
|
+
pin.style.top = `${y}px`;
|
|
5799
|
+
setPinIcon(pin, comment);
|
|
5800
|
+
const statusDot = document.createElement("span");
|
|
5801
|
+
statusDot.className = "comty-pin-status-dot";
|
|
5802
|
+
applyStatusDot(statusDot, comment.status);
|
|
5803
|
+
pin.appendChild(statusDot);
|
|
5804
|
+
return pin;
|
|
5805
|
+
}
|
|
5806
|
+
|
|
5807
|
+
function createCommentPreview(comment) {
|
|
5808
|
+
const bubble = document.createElement("div");
|
|
5809
|
+
bubble.className = "comty-bubble comty-hover-bubble";
|
|
5810
|
+
const thread = document.createElement("div");
|
|
5811
|
+
thread.className = "comty-thread";
|
|
5812
|
+
thread.appendChild(createThreadItem(comment, { showMore: false, showLink: false }).item);
|
|
5813
|
+
bubble.appendChild(thread);
|
|
5814
|
+
return bubble;
|
|
5815
|
+
}
|
|
5816
|
+
|
|
5817
|
+
function mountCaptureAnnotation(comment, { x, y }) {
|
|
5818
|
+
const host = document.createElement("div");
|
|
5819
|
+
host.dataset.comtyCaptureExclude = "";
|
|
5820
|
+
host.setAttribute("aria-hidden", "true");
|
|
5821
|
+
host.inert = true;
|
|
5822
|
+
// Ancestor opacity hides measurement without changing the components'
|
|
5823
|
+
// computed opacity. Nothing flashes or intercepts input during cloning.
|
|
5824
|
+
Object.assign(host.style, { position: "fixed", inset: "0", opacity: "0", pointerEvents: "none" });
|
|
5825
|
+
const pin = createCommentPin(comment, x, y);
|
|
5826
|
+
const bubble = createCommentPreview(comment);
|
|
5827
|
+
for (const element of [pin, bubble]) {
|
|
5828
|
+
element.dataset.comtyCaptureKeep = "";
|
|
5829
|
+
element.style.setProperty("animation", "none", "important");
|
|
5830
|
+
element.style.setProperty("transition", "none", "important");
|
|
5831
|
+
element.style.setProperty("pointer-events", "none", "important");
|
|
5832
|
+
}
|
|
5833
|
+
pin.style.display = "grid";
|
|
5834
|
+
bubble.style.display = "block";
|
|
5835
|
+
host.append(pin, bubble);
|
|
5836
|
+
root.appendChild(host);
|
|
5837
|
+
positionCommentBubble(bubble, x, y);
|
|
5838
|
+
return { elements: [pin, bubble], dispose: () => host.remove() };
|
|
5839
|
+
}
|
|
5840
|
+
|
|
5640
5841
|
function createCommentAuthor(comment) {
|
|
5641
5842
|
const author = document.createElement("div");
|
|
5642
5843
|
author.className = "comty-comment-author";
|
|
@@ -5687,25 +5888,86 @@ function initComty(options = {}) {
|
|
|
5687
5888
|
const actions = document.createElement("div");
|
|
5688
5889
|
actions.className = "comty-create-actions";
|
|
5689
5890
|
|
|
5891
|
+
const linkBtn = document.createElement("button");
|
|
5892
|
+
linkBtn.type = "button";
|
|
5893
|
+
linkBtn.className = "comty-create-link-button";
|
|
5894
|
+
linkBtn.setAttribute("aria-label", "Add link");
|
|
5895
|
+
linkBtn.innerHTML = `
|
|
5896
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
5897
|
+
<path d="M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
|
5898
|
+
<path d="M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
|
5899
|
+
</svg>
|
|
5900
|
+
`;
|
|
5901
|
+
|
|
5902
|
+
const linkEditor = document.createElement("div");
|
|
5903
|
+
linkEditor.className = "comty-create-link-editor";
|
|
5904
|
+
linkEditor.inert = true;
|
|
5905
|
+
linkEditor.setAttribute("aria-hidden", "true");
|
|
5906
|
+
const linkInput = document.createElement("input");
|
|
5907
|
+
linkInput.type = "url";
|
|
5908
|
+
linkInput.placeholder = "Paste a link";
|
|
5909
|
+
linkInput.setAttribute("aria-label", "Link URL");
|
|
5910
|
+
const linkInsert = document.createElement("button");
|
|
5911
|
+
linkInsert.type = "button";
|
|
5912
|
+
linkInsert.textContent = "Add";
|
|
5913
|
+
const linkCancel = document.createElement("button");
|
|
5914
|
+
linkCancel.type = "button";
|
|
5915
|
+
linkCancel.setAttribute("aria-label", "Cancel link");
|
|
5916
|
+
linkCancel.textContent = "×";
|
|
5917
|
+
const linkField = document.createElement("div");
|
|
5918
|
+
linkField.className = "comty-create-link-field";
|
|
5919
|
+
linkField.append(linkInput, linkCancel);
|
|
5920
|
+
linkEditor.append(linkField, linkInsert);
|
|
5921
|
+
|
|
5922
|
+
const attachment = document.createElement("div");
|
|
5923
|
+
attachment.className = "comty-create-attachment";
|
|
5924
|
+
const attachmentLink = document.createElement("a");
|
|
5925
|
+
attachmentLink.className = "comty-comment-link";
|
|
5926
|
+
attachmentLink.target = "_blank";
|
|
5927
|
+
attachmentLink.rel = "noopener noreferrer";
|
|
5928
|
+
const removeAttachment = document.createElement("button");
|
|
5929
|
+
removeAttachment.type = "button";
|
|
5930
|
+
removeAttachment.setAttribute("aria-label", "Remove attached link");
|
|
5931
|
+
removeAttachment.textContent = "×";
|
|
5932
|
+
const attachmentChip = document.createElement("div");
|
|
5933
|
+
attachmentChip.className = "comty-create-attachment-chip";
|
|
5934
|
+
attachmentChip.append(attachmentLink, removeAttachment);
|
|
5935
|
+
attachment.appendChild(attachmentChip);
|
|
5936
|
+
let attachedLink = "";
|
|
5937
|
+
|
|
5690
5938
|
const submitBtn = document.createElement("button");
|
|
5939
|
+
submitBtn.type = "button";
|
|
5691
5940
|
submitBtn.className = "comty-btn-submit";
|
|
5692
5941
|
submitBtn.setAttribute("aria-label", "Post comment");
|
|
5942
|
+
submitBtn.disabled = true;
|
|
5693
5943
|
submitBtn.innerHTML = `
|
|
5694
5944
|
<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
5945
|
<path d="M12 19V5M5 12l7-7 7 7"/>
|
|
5696
5946
|
</svg>
|
|
5697
5947
|
`;
|
|
5698
5948
|
|
|
5949
|
+
const editorDraft = draftAnchor;
|
|
5699
5950
|
const submit = async () => {
|
|
5700
5951
|
const content = textarea.value.trim();
|
|
5701
|
-
if (!content) return;
|
|
5952
|
+
if (!content || textarea.disabled) return;
|
|
5702
5953
|
|
|
5703
5954
|
submitBtn.disabled = true;
|
|
5704
5955
|
textarea.disabled = true;
|
|
5956
|
+
linkBtn.disabled = true;
|
|
5705
5957
|
|
|
5706
5958
|
payload.content = content;
|
|
5959
|
+
payload.linkUrl = attachedLink || null;
|
|
5707
5960
|
textarea.textContent = content;
|
|
5708
5961
|
|
|
5962
|
+
const context = captureContext();
|
|
5963
|
+
const point = getPinPoint(pin);
|
|
5964
|
+
const pendingId = `pending-${crypto.randomUUID()}`;
|
|
5965
|
+
// Paint the comment immediately. The network save never waits for capture work.
|
|
5966
|
+
bubble.remove();
|
|
5967
|
+
pin.remove();
|
|
5968
|
+
if (draftAnchor === editorDraft) draftAnchor = null;
|
|
5969
|
+
hideInspector();
|
|
5970
|
+
addLocalComment(pendingId, payload);
|
|
5709
5971
|
let post;
|
|
5710
5972
|
try {
|
|
5711
5973
|
post = await fetch(`${apiUrl}/api/comments`, {
|
|
@@ -5713,35 +5975,32 @@ function initComty(options = {}) {
|
|
|
5713
5975
|
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
5714
5976
|
body: JSON.stringify(payload),
|
|
5715
5977
|
});
|
|
5978
|
+
if (!post.ok) throw new Error(`Comment save failed (${post.status})`);
|
|
5979
|
+
const data = await post.json();
|
|
5980
|
+
if (!data?.id) throw new Error("Comment save returned no ID");
|
|
5981
|
+
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
5982
|
+
if (context.url === location.href) {
|
|
5983
|
+
const comment = addLocalComment(String(data.id), payload);
|
|
5984
|
+
// Start only after the save and immediate UI update; the capture yields to input.
|
|
5985
|
+
void uploadCommentScreenshot(comment, context, point);
|
|
5986
|
+
} else void loadComments({ force: true });
|
|
5716
5987
|
} catch (error) {
|
|
5717
5988
|
console.error("Comty: save failed", error);
|
|
5989
|
+
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
5990
|
+
renderSidepanelComments();
|
|
5991
|
+
void renderComments(cachedComments, { retry: false });
|
|
5992
|
+
// Keep the original editor and its text available for retry, including attachments.
|
|
5993
|
+
if (!draftAnchor) draftAnchor = editorDraft;
|
|
5994
|
+
root.append(pin, bubble);
|
|
5718
5995
|
submitBtn.disabled = false;
|
|
5719
5996
|
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 });
|
|
5997
|
+
linkBtn.disabled = false;
|
|
5998
|
+
const errorMessage = document.createElement("p");
|
|
5999
|
+
errorMessage.setAttribute("role", "alert");
|
|
6000
|
+
errorMessage.textContent = "Comment not saved. Your draft is ready to retry.";
|
|
6001
|
+
bubble.querySelector('[role="alert"]')?.remove();
|
|
6002
|
+
bubble.append(errorMessage);
|
|
6003
|
+
if (bubble.isConnected) textarea.focus();
|
|
5745
6004
|
}
|
|
5746
6005
|
};
|
|
5747
6006
|
|
|
@@ -5750,8 +6009,63 @@ function initComty(options = {}) {
|
|
|
5750
6009
|
submit();
|
|
5751
6010
|
};
|
|
5752
6011
|
|
|
6012
|
+
const closeLinkEditor = () => {
|
|
6013
|
+
bubble.classList.remove("comty-create-link-open");
|
|
6014
|
+
linkEditor.inert = true;
|
|
6015
|
+
linkEditor.setAttribute("aria-hidden", "true");
|
|
6016
|
+
linkInput.value = "";
|
|
6017
|
+
linkInput.removeAttribute("aria-invalid");
|
|
6018
|
+
linkInput.setCustomValidity("");
|
|
6019
|
+
textarea.focus();
|
|
6020
|
+
};
|
|
6021
|
+
linkBtn.onclick = () => {
|
|
6022
|
+
if (!bubble.classList.contains("comty-create-expanded")) return;
|
|
6023
|
+
bubble.classList.add("comty-create-link-open");
|
|
6024
|
+
linkEditor.inert = false;
|
|
6025
|
+
linkEditor.setAttribute("aria-hidden", "false");
|
|
6026
|
+
linkInput.focus();
|
|
6027
|
+
};
|
|
6028
|
+
linkCancel.onclick = closeLinkEditor;
|
|
6029
|
+
linkInput.oninput = () => {
|
|
6030
|
+
linkInput.removeAttribute("aria-invalid");
|
|
6031
|
+
linkInput.setCustomValidity("");
|
|
6032
|
+
};
|
|
6033
|
+
linkInput.onkeydown = (event) => {
|
|
6034
|
+
if (event.key === "Enter") { event.preventDefault(); linkInsert.click(); }
|
|
6035
|
+
if (event.key === "Escape") { event.preventDefault(); closeLinkEditor(); }
|
|
6036
|
+
};
|
|
6037
|
+
linkInsert.onclick = () => {
|
|
6038
|
+
const value = linkInput.value.trim();
|
|
6039
|
+
const candidate = /^[a-z][a-z0-9+.-]*:/i.test(value) ? value : `https://${value}`;
|
|
6040
|
+
let url;
|
|
6041
|
+
try { url = new URL(candidate); } catch { /* Report below. */ }
|
|
6042
|
+
if (!url || !["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password || /\s/.test(value) || String(url).length > 2048) {
|
|
6043
|
+
linkInput.setAttribute("aria-invalid", "true");
|
|
6044
|
+
linkInput.setCustomValidity("Enter a valid HTTP(S) URL.");
|
|
6045
|
+
linkInput.reportValidity();
|
|
6046
|
+
linkInput.focus();
|
|
6047
|
+
return;
|
|
6048
|
+
}
|
|
6049
|
+
attachedLink = String(url);
|
|
6050
|
+
attachmentLink.href = attachedLink;
|
|
6051
|
+
attachmentLink.title = attachedLink;
|
|
6052
|
+
attachmentLink.textContent = attachedLink;
|
|
6053
|
+
bubble.classList.add("comty-create-has-attachment");
|
|
6054
|
+
closeLinkEditor();
|
|
6055
|
+
const point = getPinPoint(pin);
|
|
6056
|
+
positionCommentBubble(bubble, point.x, point.y);
|
|
6057
|
+
renderInspector();
|
|
6058
|
+
};
|
|
6059
|
+
|
|
6060
|
+
removeAttachment.onclick = () => {
|
|
6061
|
+
attachedLink = "";
|
|
6062
|
+
bubble.classList.remove("comty-create-has-attachment");
|
|
6063
|
+
textarea.focus();
|
|
6064
|
+
textarea.oninput();
|
|
6065
|
+
};
|
|
6066
|
+
|
|
5753
6067
|
textarea.onkeydown = (e) => {
|
|
5754
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
6068
|
+
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
|
5755
6069
|
e.preventDefault();
|
|
5756
6070
|
submit();
|
|
5757
6071
|
}
|
|
@@ -5762,31 +6076,20 @@ function initComty(options = {}) {
|
|
|
5762
6076
|
};
|
|
5763
6077
|
|
|
5764
6078
|
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
|
-
}
|
|
6079
|
+
const expanded = textarea.value.length > 0 || Boolean(attachedLink);
|
|
6080
|
+
bubble.classList.toggle("comty-create-expanded", expanded);
|
|
6081
|
+
if (!expanded && bubble.classList.contains("comty-create-link-open")) closeLinkEditor();
|
|
6082
|
+
submitBtn.disabled = !textarea.value.trim();
|
|
6083
|
+
submitBtn.classList.toggle("comty-btn-active", !submitBtn.disabled);
|
|
5782
6084
|
const point = getPinPoint(pin);
|
|
5783
6085
|
positionCommentBubble(bubble, point.x, point.y);
|
|
5784
6086
|
renderInspector();
|
|
5785
6087
|
};
|
|
5786
6088
|
|
|
5787
|
-
actions.
|
|
6089
|
+
actions.append(linkBtn, submitBtn, linkEditor);
|
|
5788
6090
|
|
|
5789
6091
|
bubble.appendChild(textarea);
|
|
6092
|
+
bubble.appendChild(attachment);
|
|
5790
6093
|
bubble.appendChild(actions);
|
|
5791
6094
|
|
|
5792
6095
|
root.appendChild(bubble);
|
|
@@ -5895,6 +6198,7 @@ function initComty(options = {}) {
|
|
|
5895
6198
|
const item = document.createElement("button");
|
|
5896
6199
|
item.type = "button";
|
|
5897
6200
|
item.className = "comty-sidepanel-item";
|
|
6201
|
+
item.disabled = Boolean(comment.pending);
|
|
5898
6202
|
item.innerHTML = `
|
|
5899
6203
|
<span class="comty-sidepanel-item-status" aria-hidden="true"></span>
|
|
5900
6204
|
<span class="comty-sidepanel-item-body">
|
|
@@ -5915,7 +6219,7 @@ function initComty(options = {}) {
|
|
|
5915
6219
|
|
|
5916
6220
|
const metaEl = item.querySelector(".comty-sidepanel-item-meta");
|
|
5917
6221
|
if (metaEl instanceof HTMLElement) {
|
|
5918
|
-
metaEl.innerText = `${comment.author_name || "Anonymous"} · ${getCommentLabel(comment)}`;
|
|
6222
|
+
metaEl.innerText = `${comment.author_name || "Anonymous"} · ${comment.pending ? "Sending…" : getCommentLabel(comment)}`;
|
|
5919
6223
|
metaEl.title = getCommentLabel(comment);
|
|
5920
6224
|
}
|
|
5921
6225
|
|
|
@@ -6246,11 +6550,13 @@ function initComty(options = {}) {
|
|
|
6246
6550
|
const openCommentThread = createCommentThread({
|
|
6247
6551
|
root, apiUrl, projectKey, commentStatuses, selectedStatusFilters, placedPins,
|
|
6248
6552
|
closeBubbles, getCommentStatus, updateCommentStatus, applyStatusDot,
|
|
6249
|
-
renderSidepanelComments, applyStatusFilters, createThreadItem,
|
|
6553
|
+
renderSidepanelComments, applyStatusFilters, createThreadItem, renderCommentContent,
|
|
6250
6554
|
isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
|
|
6251
6555
|
updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
|
|
6252
6556
|
showAuthPrompt,
|
|
6253
6557
|
get canWrite() { return canWrite; },
|
|
6558
|
+
get canManage() { return canManage; },
|
|
6559
|
+
get allowGuestComments() { return allowGuestComments; },
|
|
6254
6560
|
get currentUser() { return currentUser; },
|
|
6255
6561
|
get cachedComments() { return cachedComments; },
|
|
6256
6562
|
set cachedComments(value) { cachedComments = value; },
|
|
@@ -6300,20 +6606,11 @@ function initComty(options = {}) {
|
|
|
6300
6606
|
const x = visibility.clientX;
|
|
6301
6607
|
const y = visibility.clientY;
|
|
6302
6608
|
|
|
6303
|
-
const pin =
|
|
6304
|
-
pin.
|
|
6305
|
-
pin.type = "button";
|
|
6609
|
+
const pin = createCommentPin(comment, x, y);
|
|
6610
|
+
pin.disabled = Boolean(comment.pending);
|
|
6306
6611
|
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);
|
|
6612
|
+
if (comment.id) pin.dataset.commentId = String(comment.id);
|
|
6613
|
+
const pinStatusDot = pin.querySelector(".comty-pin-status-dot");
|
|
6317
6614
|
pin.setAttribute(
|
|
6318
6615
|
"aria-label",
|
|
6319
6616
|
`Afficher le commentaire, statut ${getCommentStatus(comment.status).label}`,
|
|
@@ -6355,32 +6652,14 @@ function initComty(options = {}) {
|
|
|
6355
6652
|
activeHoverCommentIds.add(commentId);
|
|
6356
6653
|
}
|
|
6357
6654
|
|
|
6358
|
-
const hoverBubble =
|
|
6359
|
-
hoverBubble.className = "comty-bubble comty-hover-bubble";
|
|
6655
|
+
const hoverBubble = createCommentPreview(comment);
|
|
6360
6656
|
hoverBubble.style.left = `${x + 32}px`;
|
|
6361
6657
|
hoverBubble.style.top = `${y - 17}px`;
|
|
6362
|
-
hoverBubble.style.pointerEvents = "none";
|
|
6363
6658
|
hoverBubble.dataset.hoverId = commentId;
|
|
6364
6659
|
|
|
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
6660
|
root.appendChild(hoverBubble);
|
|
6372
6661
|
|
|
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
|
-
});
|
|
6662
|
+
requestAnimationFrame(() => positionCommentBubble(hoverBubble, x, y));
|
|
6384
6663
|
};
|
|
6385
6664
|
|
|
6386
6665
|
pin.onmouseleave = () => {
|
|
@@ -6477,6 +6756,9 @@ function initComty(options = {}) {
|
|
|
6477
6756
|
cachedComments = comments
|
|
6478
6757
|
.filter(comment => comment.kind !== "agent" && !comment.local_agent)
|
|
6479
6758
|
.map(comment => Object.assign(previous.get(String(comment.id)) || {}, comment));
|
|
6759
|
+
for (const comment of previous.values()) {
|
|
6760
|
+
if (comment.pending && !cachedComments.some(item => item.id === comment.id)) cachedComments.push(comment);
|
|
6761
|
+
}
|
|
6480
6762
|
}
|
|
6481
6763
|
|
|
6482
6764
|
async function loadComments({ force = false } = {}) {
|
|
@@ -6697,7 +6979,6 @@ function initComty(options = {}) {
|
|
|
6697
6979
|
hideInspector();
|
|
6698
6980
|
return;
|
|
6699
6981
|
}
|
|
6700
|
-
if (inspectedElement !== target) inspectExpanded = selectedElement ? inspectExpanded : false;
|
|
6701
6982
|
showInspector(target);
|
|
6702
6983
|
}, true);
|
|
6703
6984
|
document.documentElement.addEventListener("pointerleave", () => {
|
|
@@ -6754,7 +7035,6 @@ function initComty(options = {}) {
|
|
|
6754
7035
|
}
|
|
6755
7036
|
measuredRegion = null;
|
|
6756
7037
|
selectedElement = getAnchorElement(target);
|
|
6757
|
-
inspectExpanded = false;
|
|
6758
7038
|
showInspector(target);
|
|
6759
7039
|
return;
|
|
6760
7040
|
}
|
|
@@ -6912,13 +7192,26 @@ function initComty(options = {}) {
|
|
|
6912
7192
|
const data = await res.json();
|
|
6913
7193
|
projectId = data.projectId;
|
|
6914
7194
|
canWrite = Boolean(data.canWrite);
|
|
7195
|
+
canManage = Boolean(data.canManage ?? data.canWrite);
|
|
7196
|
+
allowGuestComments = Boolean(data.allowGuestComments);
|
|
6915
7197
|
currentUser = data.user || null;
|
|
6916
7198
|
loginUrl = data.loginUrl || "";
|
|
6917
7199
|
|
|
6918
7200
|
if (!canWrite && sdkToken) {
|
|
6919
|
-
sessionStorage.removeItem(tokenStorageKey);
|
|
7201
|
+
try { sessionStorage.removeItem(tokenStorageKey); } catch { /* Storage may be unavailable. */ }
|
|
6920
7202
|
sdkToken = "";
|
|
6921
7203
|
}
|
|
7204
|
+
if (didCompleteSignIn || authStatus === "forbidden") {
|
|
7205
|
+
let previousSession;
|
|
7206
|
+
try {
|
|
7207
|
+
previousSession = JSON.parse(sessionStorage.getItem(authReturnStorageKey) || "null");
|
|
7208
|
+
sessionStorage.removeItem(authReturnStorageKey);
|
|
7209
|
+
} catch { /* Fall back to the requested comment mode. */ }
|
|
7210
|
+
const mode = canWrite && ["browse", "edit", "inspect"].includes(previousSession?.mode)
|
|
7211
|
+
? previousSession.mode : canWrite ? "edit" : "browse";
|
|
7212
|
+
setSession(mode, { entry: previousSession?.entry || mode });
|
|
7213
|
+
if (previousSession?.sidepanelOpen) setSidepanelOpen(true);
|
|
7214
|
+
}
|
|
6922
7215
|
if (authStatus === "forbidden") showAuthPrompt(true);
|
|
6923
7216
|
if (canWrite && didCompleteSignIn) {
|
|
6924
7217
|
const toast = document.createElement("div");
|
|
@@ -6947,7 +7240,7 @@ function initComty(options = {}) {
|
|
|
6947
7240
|
setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
|
|
6948
7241
|
return renderComments(cachedComments, { retry: false });
|
|
6949
7242
|
},
|
|
6950
|
-
setCanWrite(value) { canWrite = value; },
|
|
7243
|
+
setCanWrite(value) { canWrite = value; canManage = value; },
|
|
6951
7244
|
invalidate() { anchorRevision += 1; },
|
|
6952
7245
|
});
|
|
6953
7246
|
}
|