@comty.io/core 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/comty-sdk.js CHANGED
@@ -12,10 +12,9 @@ function getComtyStyle() {
12
12
  --comty-foreground: #fafafa;
13
13
  --comty-muted: var(--comty-toolbar-muted);
14
14
  --comty-accent: #55aaf5;
15
- --comty-agent-warning: #e9b86d;
16
- --comty-agent-success: #80c9a4;
17
- --comty-agent-error: #ef7676;
18
- --comty-outline: oklch(0.62 0.18 255);
15
+ --comty-error: #ef7676;
16
+ --comty-outline: var(--comty-toolbar-muted);
17
+ --comty-comment-focus: var(--comty-toolbar-muted);
19
18
  --comty-paper: #fff;
20
19
  --comty-ink: #171717;
21
20
  --comty-border: #434649;
@@ -39,6 +38,10 @@ function getComtyStyle() {
39
38
  --comty-magnifier-gray: #999;
40
39
  --comty-magnifier-shadow: 0 .0625rem .375rem rgb(0 0 0 / .5);
41
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;
42
45
  --comty-property-swatch: 2rem;
43
46
  --comty-property-font-size: .6875rem;
44
47
  --comty-property-row-height: 1.375rem;
@@ -52,14 +55,11 @@ function getComtyStyle() {
52
55
  --comty-dialog-ink: var(--comty-foreground);
53
56
  --comty-dialog-soft: var(--comty-muted);
54
57
  --comty-dialog-field: var(--comty-field-surface);
55
- --comty-dialog-line: var(--comty-border);
56
58
  --comty-dialog-backdrop: rgb(0 0 0 / .4);
57
59
  --comty-dialog-radius: 2rem;
58
60
  --comty-dialog-width: 32rem;
59
61
  --comty-dialog-shadow: 0 1.5rem 4rem rgb(0 0 0 / .16);
60
62
  --comty-motion-duration: 220ms;
61
- --comty-working-dot-size: .25em;
62
- --comty-working-dot-gap: .125em;
63
63
  --comty-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
64
64
  --comty-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
65
65
  position: fixed;
@@ -71,68 +71,7 @@ function getComtyStyle() {
71
71
  font-family: var(--comty-font);
72
72
  color-scheme: dark;
73
73
  }
74
- html.comty-agent-enabled body { transition: margin-left 220ms cubic-bezier(0.23, 1, 0.32, 1), margin-right 220ms cubic-bezier(0.23, 1, 0.32, 1); }
75
- html.comty-agent-docked[data-comty-agent-side="left"] body { margin-left: 22rem; }
76
- html.comty-agent-docked[data-comty-agent-side="right"] body { margin-right: 22rem; }
77
- @media (max-width: 50rem) { html.comty-agent-docked body { margin-left: 0; margin-right: 0; } }
78
- #comty-root > .comty-agent-panel { position: fixed; left: 0; top: 0; bottom: 0; z-index: 2147483647; width: min(22rem, calc(100vw - 3rem)); box-sizing: border-box; display: flex; flex-direction: column; overflow: hidden; pointer-events: auto; border-right: .0625rem solid var(--comty-hover-surface); background: var(--comty-surface); color: var(--comty-foreground); font: .875rem/1.4 ui-sans-serif, system-ui, sans-serif; transform: translateX(0); transition: transform var(--comty-motion-duration) var(--comty-ease-out); color-scheme: dark; }
79
- #comty-root > .comty-agent-panel[data-side="right"] { left: auto; right: 0; border-right: 0; border-left: .0625rem solid var(--comty-hover-surface); }
80
- #comty-root > .comty-agent-panel[data-closed] { transform: translateX(-100%); pointer-events: none; }
81
- #comty-root > .comty-agent-panel[data-side="right"][data-closed] { transform: translateX(100%); }
82
- #comty-root > .comty-agent-panel[hidden] { display: none; }
83
- .comty-agent-panel *, .comty-agent-panel *::before { box-sizing: border-box; }
84
- .comty-agent-panel-header { display: flex; flex: 0 0 auto; align-items: center; gap: .75rem; padding: .5rem .75rem; border-bottom: .0625rem solid var(--comty-hover-surface); }
85
- .comty-agent-brand { display: grid; place-items: center; width: 2.375rem; height: 2.375rem; flex-shrink: 0; border-radius: .6875rem; color: var(--comty-accent); background: var(--comty-hover-surface); }
86
- .comty-agent-panel svg { display: block; width: 1rem; height: 1rem; flex-shrink: 0; }
87
- .comty-agent-brand svg { width: 1.25rem; height: 1.25rem; }
88
- .comty-agent-heading { flex: 1; min-width: 0; }
89
- .comty-agent-heading strong { display: block; font-weight: 600; font-size: .875rem; }
90
- .comty-agent-panel-content { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--comty-hover-surface) transparent; padding: .375rem .5rem 2rem; }
91
- .comty-agent-item { padding: .375rem .25rem; overflow-wrap: anywhere; }
92
- .comty-agent-item + .comty-agent-item { border-top: .0625rem solid var(--comty-hover-surface); }
93
- .comty-agent-item h3 { margin: 0 0 .25rem; color: var(--comty-foreground); font: 500 .875rem/1.4 ui-sans-serif, system-ui, sans-serif; }
94
- .comty-agent-list { display: grid; gap: .125rem; }
95
- .comty-agent-row { position: relative; display: flex; align-items: center; gap: .625rem; min-height: 2rem; padding: .25rem .5rem; border-radius: .5rem; color: var(--comty-foreground); }
96
- .comty-agent-row:hover { background: var(--comty-hover-surface); }
97
- .comty-agent-dot { width: .4375rem; height: .4375rem; flex: 0 0 auto; border-radius: 50%; background: var(--comty-muted); }
98
- .comty-working-indicator { display: inline-grid; grid-template-columns: repeat(3, var(--comty-working-dot-size)); gap: var(--comty-working-dot-gap); flex: 0 0 auto; color: var(--comty-foreground); }
99
- .comty-working-indicator > i { display: block; width: var(--comty-working-dot-size); height: var(--comty-working-dot-size); border-radius: 50%; background: currentColor; opacity: .25; transition: opacity 150ms linear; }
100
- .comty-working-indicator > i[data-active] { opacity: 1; }
101
- .comty-agent-dot[data-status="success"] { background: var(--comty-agent-success); }
102
- .comty-agent-dot[data-status="error"] { background: var(--comty-agent-error); }
103
- .comty-agent-row-name { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
104
- .comty-agent-row-state { flex: 0 0 auto; color: var(--comty-muted); font-size: .875rem; }
105
- .comty-agent-row-actions { position: absolute; top: 50%; right: .25rem; display: flex; gap: .25rem; border-radius: .25rem; background: var(--comty-hover-surface); opacity: 0; pointer-events: none; transform: translateY(-50%); }
106
- .comty-agent-row:hover .comty-agent-row-actions, .comty-agent-row-actions:has(.comty-agent-action:focus-visible) { opacity: 1; pointer-events: auto; }
107
- @media (hover: none) { .comty-agent-row-actions { position: static; opacity: 1; pointer-events: auto; transform: none; background: transparent; } }
108
- .comty-agent-panel .comty-agent-row-actions .comty-agent-action { min-height: 1.5rem; padding: .125rem .375rem; border-radius: .25rem; background: transparent; color: var(--comty-muted); font-size: .875rem; }
109
- .comty-agent-panel .comty-agent-row-actions .comty-agent-action:hover { color: var(--comty-foreground); background: var(--comty-surface); }
110
- .comty-agent-error-popover { position: fixed; z-index: 2147483647; width: min(20rem, calc(100vw - 1rem)); max-height: calc(100vh - 1rem); overflow: auto; padding: .75rem .875rem; border: 0; border-radius: .625rem; background: var(--comty-surface); box-shadow: var(--comty-floating-shadow); color: var(--comty-foreground); font: .875rem/1.4 ui-sans-serif, system-ui, sans-serif; pointer-events: none; color-scheme: dark; }
111
- .comty-agent-error-popover strong { display: block; font-weight: 600; }
112
- .comty-agent-error-popover p { margin: .375rem 0 0; color: var(--comty-muted); white-space: pre-wrap; overflow-wrap: anywhere; }
113
- .comty-agent-panel p { margin: .5rem 0 0; font: inherit; color: var(--comty-muted); }
114
- .comty-agent-error { margin-top: .75rem; }
115
- .comty-agent-error strong { display: block; font-weight: 500; color: var(--comty-foreground); }
116
- .comty-agent-error p { margin-top: .25rem; }
117
- .comty-agent-banner { margin-bottom: 1rem; }
118
- .comty-agent-banner .comty-agent-error { margin-top: 0; }
119
- .comty-agent-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .875rem; }
120
- .comty-agent-panel .comty-agent-action { display: inline-flex; align-items: center; justify-content: center; gap: .375rem; min-height: 2rem; margin: 0; padding: .375rem .625rem; border: 0; border-radius: .5rem; font: 500 .875rem/1.4 ui-sans-serif, system-ui, sans-serif; cursor: pointer; color: var(--comty-foreground); background: var(--comty-hover-surface); transition: background-color 150ms ease; }
121
- .comty-agent-panel .comty-agent-action:hover { background: color-mix(in srgb, var(--comty-foreground) 12%, var(--comty-hover-surface)); }
122
- .comty-agent-panel .comty-agent-action[data-primary] { color: var(--comty-paper); background: var(--comty-accent); }
123
- .comty-agent-panel .comty-agent-action:disabled { opacity: .5; cursor: wait; }
124
- .comty-agent-panel .comty-agent-close { width: 1.5rem; min-height: 1.5rem; padding: 0; background: transparent; color: var(--comty-muted); }
125
- .comty-agent-panel :is(button, summary):focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .1875rem; }
126
- .comty-agent-details { margin-top: .625rem; }
127
- .comty-agent-panel summary { cursor: pointer; color: var(--comty-muted); font-size: .875rem; }
128
- .comty-agent-panel pre { margin: .5rem 0 0; padding: .625rem; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 10rem; overflow: auto; border-radius: .5rem; background: var(--comty-surface); color: var(--comty-muted); font: .875rem/1.4 ui-monospace, monospace; }
129
- .comty-agent-empty { padding: 1rem .5rem; text-align: center; }
130
- .comty-agent-empty .comty-agent-brand { margin: 0 auto .875rem; }
131
- .comty-agent-empty strong { font-size: .875rem; font-weight: 500; }
132
- .comty-agent-empty p { max-width: 15rem; margin: .5rem auto 0; font-size: .875rem; }
133
- .comty-agent-working { position: fixed; z-index: 1; box-sizing: border-box; border: 2px solid var(--comty-accent); border-radius: .25rem; pointer-events: none; }
134
- .comty-agent-working-badge { position: absolute; left: -.125rem; bottom: 100%; display: inline-flex; align-items: center; gap: .4rem; padding: .25rem .5rem; border-radius: .375rem .375rem 0 0; background: var(--comty-surface); color: var(--comty-foreground); font: 600 .6875rem/1.4 ui-sans-serif, system-ui, sans-serif; white-space: nowrap; }
135
- .comty-agent-working[data-badge-below] .comty-agent-working-badge { bottom: auto; top: 100%; border-radius: 0 0 .375rem .375rem; }
74
+
136
75
  #comty-root { cursor: default; }
137
76
  #comty-root::backdrop { background: transparent; pointer-events: none; }
138
77
  html.comty-comment-placement,
@@ -205,8 +144,7 @@ function getComtyStyle() {
205
144
  #comty-root [hidden] { display: none !important; }
206
145
  [data-comty-anchor-hidden] { visibility: hidden !important; pointer-events: none !important; }
207
146
  @media (prefers-reduced-motion: reduce) {
208
- html.comty-agent-enabled body { transition: none; }
209
- .comty-toolbar, .comty-control-button, .comty-sidepanel, .comty-pin, .comty-bubble, .comty-agent-panel, .comty-agent-action, .comty-agent-dot, .comty-working-indicator > i { transition: none !important; animation: none !important; }
147
+ .comty-toolbar, .comty-control-button, .comty-sidepanel, .comty-pin, .comty-bubble { transition: none !important; animation: none !important; }
210
148
  }
211
149
 
212
150
  .comty-sidepanel {
@@ -391,11 +329,11 @@ function getComtyStyle() {
391
329
  .comty-pin {
392
330
  position: fixed;
393
331
  z-index: 2147483645;
394
- width: 34px;
395
- height: 34px;
332
+ width: 28px;
333
+ height: 28px;
396
334
  padding: 0;
397
335
  border: none;
398
- border-radius: 999px 999px 999px 12px;
336
+ border-radius: 999px 999px 999px 10px;
399
337
  background: #0078FF;
400
338
  color: white;
401
339
  display: grid;
@@ -405,17 +343,17 @@ function getComtyStyle() {
405
343
  transform: translate(-50%, -50%);
406
344
  appearance: none;
407
345
  font-family: sans-serif;
408
- filter: drop-shadow(0 8px 18px rgba(0, 120, 255, .28));
346
+ filter: drop-shadow(0 8px 16px rgba(0, 120, 255, .28));
409
347
  display: none;
410
348
  overflow: visible;
411
349
  }
412
350
 
413
- #comty-root > .comty-temp-pin { pointer-events: none; }
351
+ #comty-root > .comty-temp-pin { pointer-events: none; z-index: 2147483647; }
414
352
 
415
353
  .comty-pin-icon {
416
354
  display: block;
417
- width: 18px;
418
- height: 18px;
355
+ width: 16px;
356
+ height: 16px;
419
357
  pointer-events: none;
420
358
  }
421
359
 
@@ -424,11 +362,11 @@ function getComtyStyle() {
424
362
  top: -4px;
425
363
  right: -4px;
426
364
  display: inline-flex;
427
- width: 12px;
428
- height: 12px;
365
+ width: 10px;
366
+ height: 10px;
429
367
  border: 1px solid rgba(255,255,255,.82);
430
368
  border-radius: 999px;
431
- box-shadow: 0 8px 18px rgba(15,23,42,.18);
369
+ box-shadow: 0 8px 16px rgba(15,23,42,.18);
432
370
  pointer-events: none;
433
371
  }
434
372
 
@@ -436,7 +374,7 @@ function getComtyStyle() {
436
374
  background: var(--comty-surface);
437
375
  color: var(--comty-foreground);
438
376
  border: 2px solid var(--comty-border);
439
- box-shadow: 0 2px 6px rgba(0,0,0,.18);
377
+ box-shadow: 0 2px 4px rgba(0,0,0,.18);
440
378
  filter: none;
441
379
  transition:
442
380
  border-color 160ms ease,
@@ -486,14 +424,14 @@ function getComtyStyle() {
486
424
  background: var(--comty-surface);
487
425
  color: var(--comty-foreground);
488
426
  border: 0;
489
- box-shadow: 0 10px 28px rgba(0,0,0,.2);
427
+ box-shadow: 0 8px 24px rgba(0,0,0,.2);
490
428
  padding: 0;
491
- border-radius: 14px;
492
- width: min(360px, calc(100vw - 32px));
429
+ border-radius: 12px;
430
+ width: min(288px, calc(100vw - 24px));
493
431
  overflow: hidden;
494
432
  box-sizing: border-box;
495
433
  font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
496
- font-size: 14px;
434
+ font-size: 12px;
497
435
  display: none;
498
436
  animation: comty-bubble-in 200ms cubic-bezier(.23,1,.32,1) forwards;
499
437
  transform-origin: top left;
@@ -521,37 +459,53 @@ function getComtyStyle() {
521
459
  display: flex;
522
460
  align-items: center;
523
461
  justify-content: space-between;
524
- gap: 12px;
525
- min-height: 40px;
526
- padding: 0 16px;
527
- border-bottom: 1px solid var(--comty-border);
528
- }
529
-
530
- .comty-bubble-title {
531
- margin: 0;
532
- color: var(--comty-foreground);
533
- font-size: 12px;
534
- font-weight: 600;
535
- line-height: 1;
536
- }
537
-
538
- .comty-bubble-header-actions {
539
- display: flex;
540
- align-items: center;
541
- gap: 6px;
542
- margin-left: auto;
462
+ gap: 10px;
463
+ min-height: 1.5rem;
464
+ padding: .625rem .625rem 0;
543
465
  }
544
466
 
545
467
  .comty-bubble-content {
546
468
  flex: 1;
547
469
  color: var(--comty-foreground);
548
- font-size: 14px;
470
+ font-size: .75rem;
549
471
  line-height: 1.45;
550
472
  overflow-wrap: anywhere;
551
473
  cursor: text;
552
474
  white-space: pre-wrap;
553
475
  }
554
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
+
555
509
  .comty-bubble-content-editor {
556
510
  display: block;
557
511
  flex: 1;
@@ -563,7 +517,7 @@ function getComtyStyle() {
563
517
  background: transparent;
564
518
  color: var(--comty-foreground);
565
519
  cursor: text;
566
- font: 14px/1.45 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
520
+ font: .75rem/1.45 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
567
521
  outline: none;
568
522
  resize: none;
569
523
  overflow: hidden;
@@ -573,15 +527,15 @@ function getComtyStyle() {
573
527
  }
574
528
 
575
529
  .comty-thread {
576
- padding: 18px 16px 16px;
530
+ padding: .625rem;
577
531
  }
578
532
 
579
533
  .comty-thread-list {
580
534
  display: grid;
581
535
  width: 100%;
582
536
  min-width: 0;
583
- gap: 20px;
584
- max-height: min(360px, calc(100vh - 152px));
537
+ gap: 16px;
538
+ max-height: min(288px, calc(100vh - 120px));
585
539
  overflow-x: hidden;
586
540
  overflow-y: auto;
587
541
  overscroll-behavior: contain;
@@ -594,9 +548,9 @@ function getComtyStyle() {
594
548
  display: grid;
595
549
  width: 100%;
596
550
  min-width: 0;
597
- grid-template-columns: 24px minmax(0, 1fr);
551
+ grid-template-columns: 1.5rem minmax(0, 1fr);
598
552
  align-items: start;
599
- gap: 12px;
553
+ gap: 10px;
600
554
  }
601
555
 
602
556
  .comty-thread-item-body {
@@ -611,15 +565,15 @@ function getComtyStyle() {
611
565
  display: flex;
612
566
  align-items: center;
613
567
  min-width: 0;
614
- min-height: 20px;
615
- gap: 7px;
568
+ min-height: 16px;
569
+ gap: 6px;
616
570
  }
617
571
 
618
572
  .comty-comment-author-name {
619
573
  min-width: 0;
620
574
  overflow: hidden;
621
575
  color: var(--comty-foreground);
622
- font-size: 14px;
576
+ font-size: .75rem;
623
577
  font-weight: 600;
624
578
  line-height: 1.25;
625
579
  text-overflow: ellipsis;
@@ -629,7 +583,7 @@ function getComtyStyle() {
629
583
  .comty-comment-time {
630
584
  flex: 0 0 auto;
631
585
  color: var(--comty-muted);
632
- font-size: 13px;
586
+ font-size: .6875rem;
633
587
  font-weight: 400;
634
588
  line-height: 1.25;
635
589
  }
@@ -637,13 +591,13 @@ function getComtyStyle() {
637
591
  .comty-comment-avatar {
638
592
  display: grid;
639
593
  place-items: center;
640
- width: 24px;
641
- height: 24px;
594
+ width: 1.5rem;
595
+ height: 1.5rem;
642
596
  overflow: hidden;
643
597
  border-radius: 50%;
644
598
  background: var(--comty-field-surface);
645
599
  color: var(--comty-foreground);
646
- font-size: 10px;
600
+ font-size: 8px;
647
601
  font-weight: 600;
648
602
  line-height: 1;
649
603
  }
@@ -655,8 +609,8 @@ function getComtyStyle() {
655
609
  }
656
610
 
657
611
  .comty-pin-avatar {
658
- width: 28px;
659
- height: 28px;
612
+ width: 24px;
613
+ height: 24px;
660
614
  }
661
615
 
662
616
  .comty-thread-item .comty-bubble-content,
@@ -671,12 +625,12 @@ function getComtyStyle() {
671
625
  .comty-icon-button {
672
626
  display: inline-grid;
673
627
  place-items: center;
674
- width: 28px;
675
- height: 28px;
628
+ width: 24px;
629
+ height: 24px;
676
630
  flex: 0 0 auto;
677
631
  padding: 0;
678
632
  border: 0;
679
- border-radius: 6px;
633
+ border-radius: 4px;
680
634
  background: transparent;
681
635
  color: var(--comty-foreground);
682
636
  cursor: pointer;
@@ -706,24 +660,24 @@ function getComtyStyle() {
706
660
  }
707
661
 
708
662
  .comty-icon-button svg {
709
- width: 18px;
710
- height: 18px;
663
+ width: 16px;
664
+ height: 16px;
711
665
  pointer-events: none;
712
666
  }
713
667
 
714
668
  .comty-status-trigger {
715
669
  display: inline-flex;
716
670
  align-items: center;
717
- gap: 6px;
718
- min-height: 28px;
671
+ gap: 4px;
672
+ min-height: 24px;
719
673
  padding: 0 8px;
720
674
  border: 0;
721
- border-radius: 6px;
675
+ border-radius: 4px;
722
676
  background: transparent;
723
677
  color: var(--comty-foreground);
724
678
  cursor: pointer;
725
679
  appearance: none;
726
- font: 600 12px/1 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
680
+ font: 600 .6875rem/1 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
727
681
  transition:
728
682
  background-color 160ms ease,
729
683
  transform 140ms ease-out;
@@ -752,14 +706,14 @@ function getComtyStyle() {
752
706
  }
753
707
 
754
708
  .comty-thread-more {
755
- width: 24px;
756
- height: 24px;
757
- margin: -2px -4px -2px auto;
709
+ width: 20px;
710
+ height: 20px;
711
+ margin: 0 0 0 auto;
758
712
  }
759
713
 
760
714
  .comty-thread-more svg {
761
- width: 16px;
762
- height: 16px;
715
+ width: 12px;
716
+ height: 12px;
763
717
  }
764
718
 
765
719
  .comty-entry-actions-popover {
@@ -767,49 +721,68 @@ function getComtyStyle() {
767
721
  }
768
722
 
769
723
  .comty-popover-item.comty-popover-item-destructive {
770
- color: var(--comty-agent-error);
724
+ color: var(--comty-error);
771
725
  }
772
726
 
773
727
  .comty-popover-item.comty-popover-item-destructive:hover,
774
728
  .comty-popover-item.comty-popover-item-destructive:focus-visible {
775
- background: color-mix(in srgb, var(--comty-agent-error) 12%, var(--comty-surface));
729
+ background: color-mix(in srgb, var(--comty-error) 12%, var(--comty-surface));
776
730
  outline: none;
777
731
  }
778
732
 
779
733
  .comty-reply-form {
780
734
  display: grid;
781
- grid-template-columns: 24px minmax(0, 1fr);
735
+ grid-template-columns: 1.5rem minmax(0, 1fr);
782
736
  align-items: center;
783
- gap: 12px;
784
- margin-top: 20px;
737
+ gap: 10px;
738
+ margin-top: 16px;
785
739
  }
786
740
 
787
741
  .comty-reply-field {
788
742
  display: flex;
789
743
  align-items: center;
790
- min-height: 36px;
791
- padding: 4px 8px 4px 16px;
792
- border-radius: 14px;
744
+ min-height: 1.875rem;
745
+ padding: .25rem .25rem .25rem .625rem;
746
+ border-radius: 1.25rem;
793
747
  background: var(--comty-field-surface);
794
748
  box-sizing: border-box;
795
- 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;
796
760
  }
797
761
 
798
762
  .comty-reply-field:focus-within {
799
- box-shadow: inset 0 0 0 1px #a3a3a3;
763
+ box-shadow: inset 0 0 0 .0625rem var(--comty-comment-focus);
800
764
  }
801
765
 
802
766
  .comty-reply-input {
803
767
  min-width: 0;
768
+ min-height: 0;
804
769
  flex: 1;
805
770
  padding: 0;
806
771
  border: 0;
772
+ height: 1.125rem;
773
+ max-height: 5rem;
807
774
  background: transparent;
808
775
  color: var(--comty-foreground);
809
- font: 14px/1.3 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
776
+ font: .6875rem/1.125rem Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
810
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);
811
782
  }
812
783
 
784
+ .comty-reply-expanded .comty-reply-input { overflow-y: auto; }
785
+
813
786
  .comty-reply-input::placeholder {
814
787
  color: var(--comty-muted);
815
788
  }
@@ -817,14 +790,14 @@ function getComtyStyle() {
817
790
  .comty-reply-button {
818
791
  display: inline-grid;
819
792
  place-items: center;
820
- width: 28px;
821
- height: 28px;
793
+ width: 1.375rem;
794
+ height: 1.375rem;
822
795
  flex: 0 0 auto;
823
796
  padding: 0;
824
797
  border: 0;
825
798
  border-radius: 50%;
826
- background: #0d99ff;
827
- color: #fff;
799
+ background: var(--comty-foreground);
800
+ color: var(--comty-surface);
828
801
  cursor: pointer;
829
802
  transition:
830
803
  background-color 160ms ease,
@@ -833,7 +806,7 @@ function getComtyStyle() {
833
806
  }
834
807
 
835
808
  .comty-reply-button:hover:not(:disabled) {
836
- background: #0b87e3;
809
+ background: var(--comty-muted);
837
810
  }
838
811
 
839
812
  .comty-reply-button:active:not(:disabled) {
@@ -841,13 +814,14 @@ function getComtyStyle() {
841
814
  }
842
815
 
843
816
  .comty-reply-button:disabled {
817
+ color: var(--comty-muted);
844
818
  background: var(--comty-hover-surface);
845
819
  cursor: default;
846
820
  }
847
821
 
848
822
  .comty-reply-button svg {
849
- width: 16px;
850
- height: 16px;
823
+ width: 12px;
824
+ height: 12px;
851
825
  }
852
826
 
853
827
  .comty-reply-form-guest { display: block; }
@@ -855,20 +829,20 @@ function getComtyStyle() {
855
829
  .comty-sign-in-reply {
856
830
  display: block;
857
831
  width: 100%;
858
- min-height: 36px;
859
- padding: 4px 16px;
832
+ min-height: 32px;
833
+ padding: 4px 12px;
860
834
  border: 0;
861
- border-radius: 14px;
835
+ border-radius: 12px;
862
836
  background: var(--comty-field-surface);
863
- color: #0d99ff;
837
+ color: var(--comty-foreground);
864
838
  text-align: left;
865
- font: 600 14px/1.3 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
839
+ font: 600 12px/1.3 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
866
840
  cursor: pointer;
867
841
  transition: background-color 160ms ease;
868
842
  }
869
843
 
870
844
  .comty-sign-in-reply:hover { background: var(--comty-hover-surface); }
871
- .comty-sign-in-reply:focus-visible { outline: 2px solid #0d99ff; outline-offset: 2px; }
845
+ .comty-sign-in-reply:focus-visible { outline: .0625rem solid var(--comty-comment-focus); outline-offset: .0625rem; }
872
846
 
873
847
  .comty-hover-bubble {
874
848
  width: min(240px, calc(100vw - 32px));
@@ -894,10 +868,6 @@ function getComtyStyle() {
894
868
  height: 24px;
895
869
  }
896
870
 
897
- .comty-hover-bubble .comty-comment-author-name {
898
- font-size: 12px;
899
- }
900
-
901
871
  .comty-hover-bubble .comty-comment-author {
902
872
  min-height: 0;
903
873
  }
@@ -906,11 +876,6 @@ function getComtyStyle() {
906
876
  font-size: 12px;
907
877
  }
908
878
 
909
- .comty-hover-bubble .comty-thread-item .comty-bubble-content {
910
- margin-top: 0;
911
- font-size: 12px;
912
- }
913
-
914
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); }
915
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; }
916
881
  .comty-auth-dialog *, .comty-auth-dialog *::before, .comty-auth-dialog *::after { box-sizing: border-box; }
@@ -921,13 +886,23 @@ function getComtyStyle() {
921
886
  .comty-auth-header svg { width: 1.25rem; height: 1.25rem; }
922
887
  .comty-auth-dialog h2 { margin: 0; color: inherit; font: 600 1.5rem/1.25 var(--comty-font); letter-spacing: -.025em; }
923
888
  .comty-auth-dialog p { margin: 1rem 0 0; color: var(--comty-dialog-soft); font: 400 .875rem/1.65 var(--comty-font); }
924
- .comty-auth-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; margin-top: 1.75rem; padding-top: 1.5rem; border-top: .0625rem solid var(--comty-dialog-line); }
889
+ .comty-auth-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; margin-top: 1.75rem; }
925
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; }
926
891
  .comty-auth-actions button:hover, .comty-auth-close:hover { background: var(--comty-dialog-field); color: var(--comty-dialog-ink); }
927
892
  .comty-auth-actions .comty-auth-primary { border-color: transparent; background: var(--comty-dialog-ink); color: var(--comty-dialog-surface); }
928
893
  .comty-auth-actions .comty-auth-primary:hover { background: var(--comty-dialog-soft); color: var(--comty-dialog-surface); }
929
894
  .comty-auth-dialog button:focus-visible { outline: .125rem solid var(--comty-dialog-ink); outline-offset: .1875rem; }
930
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; }
931
906
  .comty-distance-layer { position: fixed; inset: 0; z-index: 1; width: 100%; height: 100%; overflow: visible; pointer-events: none; color: var(--comty-measure); }
932
907
  .comty-distance-line, .comty-distance-extension { stroke: currentColor; stroke-width: 1; fill: none; shape-rendering: crispEdges; }
933
908
  .comty-distance-extension { stroke-dasharray: 3 3; opacity: .65; }
@@ -993,8 +968,6 @@ function getComtyStyle() {
993
968
  justify-content: space-between;
994
969
  gap: 8px;
995
970
  margin-top: 12px;
996
- padding-top: 10px;
997
- border-top: 1px solid var(--comty-border);
998
971
  }
999
972
 
1000
973
  .comty-status-label {
@@ -1076,6 +1049,12 @@ function getComtyStyle() {
1076
1049
  transform-origin: top right;
1077
1050
  }
1078
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
+
1079
1058
  .comty-popover-item {
1080
1059
  display: flex;
1081
1060
  align-items: center;
@@ -1191,11 +1170,11 @@ function getComtyStyle() {
1191
1170
  .comty-property-row button > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }
1192
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; }
1193
1172
  .comty-property-copy-icon { position: relative; flex: 0 0 .75rem; width: .75rem; height: .75rem; color: var(--comty-muted); opacity: .65; }
1194
- .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; }
1195
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); }
1196
- .comty-property-copy-icon svg:last-child { opacity: 0; transform: scale(.5); }
1197
- .comty-property-row button[data-copied] svg:first-child { opacity: 0; transform: scale(.5); }
1198
- .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); }
1199
1178
  @media (prefers-reduced-motion: reduce) { .comty-property-copy-icon svg { transition: none; } }
1200
1179
  .comty-property-row button[disabled] { cursor: default; opacity: .5; }
1201
1180
  .comty-property-strip { display: flex; align-items: center; justify-content: center; gap: .375rem; }
@@ -1246,29 +1225,29 @@ function getComtyStyle() {
1246
1225
  .comty-inspect-gap { --comty-spacing-color: var(--comty-gap-fill); }
1247
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; }
1248
1227
  .comty-inspector {
1249
- position: fixed; width: 15rem; max-width: calc(100vw - 1rem);
1250
- padding-block: .5rem; border-radius: .5rem;
1251
- color: var(--comty-foreground); background: var(--comty-surface);
1252
- box-shadow: var(--comty-floating-shadow); font: .625rem/1.5 ui-sans-serif, system-ui, sans-serif;
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);
1253
1232
  pointer-events: none; box-sizing: border-box;
1254
1233
  }
1255
1234
  .comty-inspector[data-selected] { pointer-events: auto; user-select: text; }
1256
- .comty-inspector-head, .comty-inspector-row { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; padding-inline: .5rem; }
1257
- .comty-inspector-head { margin-bottom: .5rem; }
1258
- .comty-inspector-head button:last-child { flex-shrink: 0; }
1259
- .comty-inspector-head button:first-child { flex: 1; font-family: ui-monospace, monospace; font-weight: 500; text-align: left; }
1260
- .comty-inspector-row span { color: var(--comty-muted); flex-shrink: 0; }
1261
- .comty-inspector button { min-width: 0; padding: 0; border: 0; color: inherit; background: transparent; font: inherit; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; cursor: pointer; }
1262
- .comty-inspector button:hover { text-decoration: underline; }
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; }
1263
1242
  .comty-inspector button:focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .125rem; }
1264
- .comty-inspector-value { display: flex; flex-direction: column; align-items: flex-end; min-width: 0; }
1265
- .comty-inspector-value > button, .comty-inspector-tokens button { max-width: 100%; }
1266
- .comty-inspector-picker { display: inline-flex; align-items: center; gap: .25rem; }
1267
- .comty-inspector-swatch { display: inline-block; flex: 0 0 .625rem; width: .625rem; height: .625rem; border: .0625rem solid var(--comty-border); border-radius: .1875rem; }
1268
- .comty-inspector-tokens { display: flex; flex-direction: column; align-items: flex-end; max-width: 100%; }
1269
- .comty-inspector-tokens button { font-family: ui-monospace, monospace; color: var(--comty-muted); }
1270
- .comty-inspector-details { max-height: 50vh; overflow-y: auto; }
1271
- .comty-inspector .comty-inspector-expand { display: block; margin: .5rem auto 0; color: var(--comty-muted); }
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; } }
1272
1251
  .comty-measure-line { position: fixed; background: var(--comty-outline); pointer-events: none; }
1273
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; }
1274
1253
 
@@ -1277,85 +1256,235 @@ function getComtyStyle() {
1277
1256
  z-index: 2147483647;
1278
1257
  background: var(--comty-surface);
1279
1258
  border: 0;
1280
- box-shadow: 0 4px 20px rgba(0,0,0,.1);
1281
- border-radius: 999px;
1282
- width: 320px;
1283
- font-family: sans-serif;
1284
- display: flex;
1285
- align-items: center;
1286
- padding: 4px 4px 4px 16px;
1287
- 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);
1288
1265
  box-sizing: border-box;
1289
- animation: comty-bubble-in 220ms cubic-bezier(.2,.8,.2,1) forwards;
1266
+ animation: comty-bubble-in 200ms var(--comty-ease-out) forwards;
1290
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);
1291
1269
  }
1292
1270
 
1293
- .comty-create-bubble.comty-bubble-left {
1294
- animation-name: comty-bubble-in-left;
1295
- transform-origin: top right;
1271
+ .comty-create-bubble.comty-create-expanded {
1272
+ height: 5.75rem;
1273
+ border-radius: .75rem;
1296
1274
  }
1297
1275
 
1276
+ .comty-create-bubble.comty-create-has-attachment { height: 7.5rem; }
1277
+ .comty-create-bubble.comty-bubble-left { transform-origin: top right; }
1278
+
1298
1279
  .comty-create-textarea {
1299
- flex: 1;
1300
- height: 24px;
1301
- min-height: 24px;
1302
- max-height: 150px;
1280
+ position: absolute;
1281
+ top: .5rem;
1282
+ left: .75rem;
1283
+ width: calc(100% - 3.25rem);
1284
+ height: 1.25rem;
1303
1285
  border: none;
1304
1286
  padding: 0;
1305
- font-family: inherit;
1306
- font-size: 14px;
1307
- line-height: 24px;
1287
+ font: inherit;
1308
1288
  resize: none;
1309
1289
  box-sizing: border-box;
1310
1290
  outline: none;
1311
1291
  background: transparent;
1312
- overflow-y: auto;
1292
+ overflow-y: hidden;
1313
1293
  color: var(--comty-foreground);
1314
1294
  scrollbar-width: thin;
1315
- scrollbar-color: gray transparent;
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;
1316
1312
  }
1317
1313
 
1318
- .comty-create-textarea::placeholder {
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;
1319
1333
  color: var(--comty-muted);
1334
+ cursor: pointer;
1320
1335
  }
1321
1336
 
1337
+ .comty-create-textarea::placeholder { color: var(--comty-muted); }
1338
+
1322
1339
  .comty-create-actions {
1340
+ position: absolute;
1341
+ right: 0;
1342
+ bottom: 0;
1343
+ left: 0;
1323
1344
  display: flex;
1324
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 {
1325
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;
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;
1326
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; }
1327
1463
 
1328
1464
  .comty-btn-submit {
1329
1465
  display: flex;
1330
1466
  align-items: center;
1331
1467
  justify-content: center;
1332
- width: 32px;
1333
- height: 32px;
1468
+ width: 1.5rem;
1469
+ height: 1.5rem;
1334
1470
  padding: 0;
1335
1471
  border-radius: 50%;
1336
1472
  background: var(--comty-border);
1337
- color: white;
1473
+ color: var(--comty-muted);
1338
1474
  border: none;
1339
1475
  cursor: pointer;
1340
- transition: background 150ms ease, opacity 150ms ease;
1341
- }
1342
-
1343
- .comty-btn-submit svg {
1344
- width: 16px;
1345
- height: 16px;
1346
- }
1347
-
1348
- .comty-btn-submit.comty-btn-active {
1349
- background: #0078FF;
1350
- }
1351
-
1352
- .comty-btn-submit.comty-btn-active:hover {
1353
- background: #0062d1;
1476
+ transition: background-color 160ms var(--comty-ease-out), color 160ms var(--comty-ease-out), opacity 160ms var(--comty-ease-out);
1354
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; }
1355
1481
 
1356
- .comty-btn-submit:disabled {
1357
- opacity: 0.6;
1358
- 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; }
1359
1488
  }
1360
1489
  `;
1361
1490
  }
@@ -1747,477 +1876,380 @@ function createAnchorMatcher({
1747
1876
  return { matchesAnchorContext, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
1748
1877
  }
1749
1878
 
1750
- function createScreenshotCapture() {
1751
- function nextAnimationFrame() {
1752
- return new Promise((resolve) => {
1753
- requestAnimationFrame(() => resolve());
1754
- });
1755
- }
1756
-
1757
- async function waitForPaint() {
1758
- await nextAnimationFrame();
1759
- await nextAnimationFrame();
1760
- }
1761
-
1762
- async function waitForCustomElements(el, timeout = 1600) {
1763
- if (
1764
- !(el instanceof Element) ||
1765
- !window.customElements ||
1766
- typeof window.customElements.whenDefined !== "function"
1767
- ) {
1768
- return;
1769
- }
1770
-
1771
- const names = new Set();
1772
- let current = el;
1773
- while (current && current instanceof Element) {
1774
- const tagName = current.tagName.toLowerCase();
1775
- if (tagName.includes("-")) {
1776
- names.add(tagName);
1777
- }
1778
- current = current.parentElement;
1779
- }
1780
-
1781
- if (names.size === 0) return;
1782
-
1783
- const definitions = [...names].map((name) =>
1784
- window.customElements.whenDefined(name).catch(() => undefined),
1785
- );
1786
- await Promise.race([
1787
- Promise.all(definitions),
1788
- new Promise((resolve) => setTimeout(resolve, timeout)),
1789
- ]);
1790
- }
1791
-
1792
- function syncFormValues(sourceRoot, cloneRoot) {
1793
- const sourceFields = sourceRoot.querySelectorAll(
1794
- "input, textarea, select",
1795
- );
1796
- const clonedFields = cloneRoot.querySelectorAll(
1797
- "input, textarea, select",
1798
- );
1799
-
1800
- sourceFields.forEach((sourceField, index) => {
1801
- const clonedField = clonedFields[index];
1802
- if (!clonedField) return;
1803
-
1804
- if (sourceField instanceof HTMLTextAreaElement) {
1805
- clonedField.textContent = sourceField.value;
1806
- clonedField.setAttribute("value", sourceField.value);
1807
- return;
1808
- }
1809
-
1810
- if (sourceField instanceof HTMLInputElement) {
1811
- clonedField.setAttribute("value", sourceField.value);
1812
- if (sourceField.checked) {
1813
- clonedField.setAttribute("checked", "");
1814
- } else {
1815
- clonedField.removeAttribute("checked");
1816
- }
1817
- return;
1818
- }
1819
-
1820
- if (sourceField instanceof HTMLSelectElement) {
1821
- Array.from(sourceField.options).forEach((option, optionIndex) => {
1822
- const clonedOption = clonedField.options[optionIndex];
1823
- if (!clonedOption) return;
1824
- if (option.selected) {
1825
- clonedOption.setAttribute("selected", "");
1826
- } else {
1827
- clonedOption.removeAttribute("selected");
1828
- }
1829
- });
1830
- }
1831
- });
1832
- }
1833
-
1834
- function toAbsoluteUrl(value) {
1835
- if (!value || value.startsWith("data:") || value.startsWith("blob:")) {
1836
- return value;
1837
- }
1838
-
1839
- try {
1840
- return new URL(value, window.location.href).toString();
1841
- } catch {
1842
- return value;
1843
- }
1844
- }
1845
-
1846
- function isExportSafeUrl(value) {
1847
- if (!value || value.startsWith("data:")) return true;
1848
-
1879
+ const captureEngine = (() => { // Generated from html-to-image 1.11.13 by scripts/capture-engine.mjs.
1880
+ /*
1881
+ MIT License
1882
+
1883
+ Copyright (c) 2017-2025 W.Y.
1884
+
1885
+ Permission is hereby granted, free of charge, to any person obtaining a copy
1886
+ of this software and associated documentation files (the "Software"), to deal
1887
+ in the Software without restriction, including without limitation the rights
1888
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1889
+ copies of the Software, and to permit persons to whom the Software is
1890
+ furnished to do so, subject to the following conditions:
1891
+
1892
+ The above copyright notice and this permission notice shall be included in all
1893
+ copies or substantial portions of the Software.
1894
+
1895
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1896
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1897
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1898
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1899
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1900
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1901
+ SOFTWARE.
1902
+
1903
+ */
1904
+ function S(t,e){if(t.match(/^[a-z]+:\/\//i))return t;if(t.match(/^\/\//))return window.location.protocol+t;if(t.match(/^[a-z]+:/i))return t;let r=document.implementation.createHTMLDocument(),n=r.createElement("base"),i=r.createElement("a");return r.head.appendChild(n),r.body.appendChild(i),e&&(n.href=e),i.href=t,i.href}var P=(()=>{let t=0,e=()=>`0000${(Math.random()*36**4<<0).toString(36)}`.slice(-4);return()=>(t+=1,`u${e()}${t}`)})();function u(t){let e=[];for(let r=0,n=t.length;r<n;r++)e.push(t[r]);return e}var f=null;function y(t={}){return f||(t.includeStyleProperties?(f=t.includeStyleProperties,f):(f=u(window.getComputedStyle(document.documentElement)),f))}function w(t){return new Promise((e,r)=>{let n=new Image;n.onload=()=>{n.decode().then(()=>{requestAnimationFrame(()=>e(n))})},n.onerror=r,n.crossOrigin="anonymous",n.decoding="async",n.src=t})}async function D(t){return Promise.resolve().then(()=>new XMLSerializer().serializeToString(t)).then(encodeURIComponent).then(e=>`data:image/svg+xml;charset=utf-8,${e}`)}async function I(t,e,r){let n="http://www.w3.org/2000/svg",i=document.createElementNS(n,"svg"),o=document.createElementNS(n,"foreignObject");return i.setAttribute("width",`${e}`),i.setAttribute("height",`${r}`),i.setAttribute("viewBox",`0 0 ${e} ${r}`),o.setAttribute("width","100%"),o.setAttribute("height","100%"),o.setAttribute("x","0"),o.setAttribute("y","0"),o.setAttribute("externalResourcesRequired","true"),i.appendChild(o),o.appendChild(t),D(i)}var l=(t,e)=>{if(t instanceof e)return!0;let r=Object.getPrototypeOf(t);return r===null?!1:r.constructor.name===e.name||l(r,e)};function U(t){let e=t.getPropertyValue("content");return`${t.cssText} content: '${e.replace(/'|"/g,"")}';`}function V(t,e){return y(e).map(r=>{let n=t.getPropertyValue(r),i=t.getPropertyPriority(r);return`${r}: ${n}${i?" !important":""};`}).join(" ")}function M(t,e,r,n){let i=`.${t}:${e}`,o=r.cssText?U(r):V(r,n);return document.createTextNode(`${i}{${o}}`)}function R(t,e,r,n){let i=window.getComputedStyle(t,r),o=i.getPropertyValue("content");if(o===""||o==="none")return;let a=P();try{e.className=`${e.className} ${a}`}catch{return}let c=document.createElement("style");c.appendChild(M(a,r,i,n)),e.appendChild(c)}function C(t,e,r){R(t,e,":before",r),R(t,e,":after",r)}var T="application/font-woff",$="image/jpeg",F={woff:T,woff2:T,ttf:"application/font-truetype",eot:"application/vnd.ms-fontobject",png:"image/png",jpg:$,jpeg:$,gif:"image/gif",tiff:"image/tiff",svg:"image/svg+xml",webp:"image/webp"};function H(t){let e=/\.([^./]*?)$/g.exec(t);return e?e[1]:""}function m(t){let e=H(t).toLowerCase();return F[e]||""}function O(t){return t.split(/,/)[1]}function p(t){return t.search(/^(data:)/)!==-1}function x(t,e){return`data:${e};base64,${t}`}async function k(t,e,r){let n=await(e?.captureFetch||fetch)(t,e);if(!n.ok)throw new Error(`Resource "${n.url}" not found`);let i=await n.blob();return new Promise((o,a)=>{let c=new FileReader;c.onerror=a,c.onloadend=()=>{try{o(r({res:n,result:c.result}))}catch(s){a(s)}},c.readAsDataURL(i)})}async function h(t,e,r){let n=r.resourceCache||{},i=t;if(n[i]!=null)return n[i];r.cacheBust&&(t+=(/\?/.test(t)?"&":"?")+new Date().getTime());let o;try{let a=await k(t,r.fetchRequestInit,({res:c,result:s})=>(e||(e=c.headers.get("Content-Type")||""),O(s)));o=x(a,e)}catch(a){o=r.imagePlaceholder||"";let c=`Failed to fetch resource: ${t}`;a&&(c=typeof a=="string"?a:a.message),c&&console.warn(c)}return n[i]=o,o}async function q(t){let e=t.toDataURL();return e==="data:,"?t.cloneNode(!1):w(e)}async function _(t,e){if(t.currentSrc){let o=document.createElement("canvas"),a=o.getContext("2d");o.width=t.clientWidth,o.height=t.clientHeight,a?.drawImage(t,0,0,o.width,o.height);let c=o.toDataURL();return w(c)}let r=t.poster,n=m(r),i=await h(r,n,e);return w(i)}async function v(t,e){var r;try{if(!((r=t?.contentDocument)===null||r===void 0)&&r.body)return await b(t.contentDocument.body,e,!0)}catch{}return t.cloneNode(!1)}async function B(t,e){return l(t,HTMLCanvasElement)?q(t).catch(()=>t.cloneNode(!1)):l(t,HTMLVideoElement)?_(t,e).catch(()=>document.createElement("div")):l(t,HTMLIFrameElement)?v(t,e):t.cloneNode(!1)}var G=t=>t.tagName!=null&&t.tagName.toUpperCase()==="SLOT";async function j(t,e,r){var n,i;let o=[];return G(t)&&t.assignedNodes?o=u(t.assignedNodes()):l(t,HTMLIFrameElement)&&(!((n=t.contentDocument)===null||n===void 0)&&n.body)?o=u(t.contentDocument.body.childNodes):o=u(((i=t.shadowRoot)!==null&&i!==void 0?i:t).childNodes),o.length===0||l(t,HTMLVideoElement)||await o.reduce((a,c)=>a.then(()=>b(c,r)).then(s=>{s&&e.appendChild(s)}),Promise.resolve()),e}function z(t,e,r){let n=e.style;if(!n)return;let i=window.getComputedStyle(t);i.cssText?(n.cssText=i.cssText,n.transformOrigin=i.transformOrigin):y(r).forEach(o=>{let a=i.getPropertyValue(o);o==="font-size"&&a.endsWith("px")&&(a=`${parseFloat(a.substring(0,a.length-2))}px`),l(t,HTMLIFrameElement)&&o==="display"&&a==="inline"&&(a="block"),o==="d"&&e.getAttribute("d")&&(a=`path(${e.getAttribute("d")})`),n.setProperty(o,a,i.getPropertyPriority(o))})}function W(t,e){l(t,HTMLTextAreaElement)&&(e.textContent=t.value),l(t,HTMLInputElement)&&e.setAttribute("value",t.value)}function X(t,e){if(l(t,HTMLSelectElement)){let n=Array.from(e.children).find(i=>t.value===i.getAttribute("value"));n&&n.setAttribute("selected","")}}function K(t,e,r){return l(e,Element)&&(z(t,e,r),C(t,e,r),W(t,e),X(t,e)),e}async function J(t,e){let r=t.querySelectorAll?t.querySelectorAll("use"):[];if(r.length===0)return t;let n={};for(let o=0;o<r.length;o++){let a=r[o],c=a.getAttribute("href")||a.getAttribute("xlink:href");if(c){let s=t.querySelector(c),d=document.querySelector(c);!s&&d&&!n[c]&&(n[c]=await b(d,e,!0))}}let i=Object.values(n);if(i.length){let o="http://www.w3.org/1999/xhtml",a=document.createElementNS(o,"svg");a.setAttribute("xmlns",o),a.style.position="absolute",a.style.width="0",a.style.height="0",a.style.overflow="hidden",a.style.display="none";let c=document.createElementNS(o,"defs");a.appendChild(c);for(let s=0;s<i.length;s++)c.appendChild(i[s]);t.appendChild(a)}return t}async function b(t,e,r){if(await e.beforeClone?.(t),!r&&e.filter&&!e.filter(t))return null;let n=e.placeholder?.(t);return n||Promise.resolve(t).then(i=>B(i,e)).then(i=>j(t,i,e)).then(async i=>(await e.beforeClone?.(t),K(t,i,e))).then(i=>e.afterClone?.(t,i)??i).then(i=>J(i,e))}var L=/url\((['"]?)([^'"]+?)\1\)/g,Q=/url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g,Y=/src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g;function Z(t){let e=t.replace(/([.*+?^${}()|\[\]\/\\])/g,"\\$1");return new RegExp(`(url\\(['"]?)(${e})(['"]?\\))`,"g")}function N(t){let e=[];return t.replace(L,(r,n,i)=>(e.push(i),r)),e.filter(r=>!p(r)&&!r.startsWith("#"))}async function tt(t,e,r,n,i){try{let o=r?S(e,r):e,a=m(e),c;if(i){let s=await i(o);c=x(s,a)}else c=await h(o,a,n);return t.replace(Z(e),`$1${c}$3`)}catch{}return t}function et(t,{preferredFontFormat:e}){return e?t.replace(Y,r=>{for(;;){let[n,,i]=Q.exec(r)||[];if(!i)return"";if(i===e)return`src: ${n};`}}):t}function rt(t){return t.search(L)!==-1}async function E(t,e,r){if(!rt(t))return t;let n=et(t,r);return N(n).reduce((o,a)=>o.then(c=>tt(c,a,e,r)),Promise.resolve(n))}async function g(t,e,r){var n;let i=(n=e.style)===null||n===void 0?void 0:n.getPropertyValue(t);if(i){let o=await E(i,null,r);return e.style.setProperty(t,o,e.style.getPropertyPriority(t)),!0}return!1}async function nt(t,e){await g("background",t,e)||await g("background-image",t,e),await g("mask",t,e)||await g("-webkit-mask",t,e)||await g("mask-image",t,e)||await g("-webkit-mask-image",t,e)}async function it(t,e){let r=l(t,HTMLImageElement);if(!(r&&!p(t.src))&&!(l(t,SVGImageElement)&&!p(t.href.baseVal)))return;let n=r?t.src:t.href.baseVal,i=await h(n,m(n),e);await new Promise((o,a)=>{t.onload=o,t.onerror=e.onImageErrorHandler?(...s)=>{try{o(e.onImageErrorHandler(...s))}catch(d){a(d)}}:a;let c=t;c.decode&&(c.decode=o),c.loading==="lazy"&&(c.loading="eager"),r?(t.srcset="",t.src=i):t.href.baseVal=i})}async function ot(t,e){let n=u(t.childNodes).map(i=>A(i,e));await Promise.all(n).then(()=>t)}async function A(t,e){await e.beforeClone?.(t),l(t,Element)&&(await nt(t,e),await it(t,e),await ot(t,e))} return {cloneNode:b,embedImages:A,embedResources:E,nodeToDataURL:I}; })();
1905
+ // Fetches belong to one capture. Never forward SDK credentials to a resource host.
1906
+ function createCaptureResources({ apiUrl, projectKey, getHeaders, signal }) {
1907
+ const cache = new Map();
1908
+ const waiting = [];
1909
+ let active = 0;
1910
+ let totalBytes = 0;
1911
+ async function fetchResource(value) {
1912
+ const url = new URL(value, document.baseURI);
1913
+ if (!["http:", "https:", "blob:", "data:"].includes(url.protocol)) throw new Error("Unsupported capture resource");
1914
+ if (cache.has(url.href)) return (await cache.get(url.href)).clone();
1915
+ if (cache.size >= 128) throw new Error("Too many capture resources");
1916
+ const pending = (async () => {
1917
+ if (active >= 4) await new Promise(resolve => waiting.push(resolve));
1918
+ active++;
1849
1919
  try {
1850
- const url = new URL(value, window.location.href);
1851
- return url.origin === window.location.origin;
1852
- } catch {
1853
- return false;
1854
- }
1855
- }
1856
-
1857
- function blobToDataUrl(blob) {
1858
- return new Promise((resolve, reject) => {
1859
- const reader = new FileReader();
1860
- reader.onload = () => resolve(String(reader.result ?? ""));
1861
- reader.onerror = reject;
1862
- reader.readAsDataURL(blob);
1863
- });
1864
- }
1865
-
1866
- async function urlToDataUrl(value) {
1867
- const absoluteUrl = toAbsoluteUrl(value);
1868
- if (!absoluteUrl || absoluteUrl.startsWith("data:")) return absoluteUrl;
1869
- if (absoluteUrl.startsWith("blob:")) return "";
1870
-
1871
- try {
1872
- const response = await fetch(absoluteUrl, {
1873
- credentials: isExportSafeUrl(absoluteUrl) ? "same-origin" : "omit",
1874
- mode: "cors",
1875
- });
1876
- if (!response.ok) return "";
1877
- return await blobToDataUrl(await response.blob());
1878
- } catch {
1879
- return "";
1880
- }
1881
- }
1882
-
1883
- async function inlineImageResources(clone) {
1884
- const images = Array.from(clone.querySelectorAll("img"));
1885
- await Promise.all(
1886
- images.map(async (image) => {
1887
- const dataUrl = await urlToDataUrl(image.getAttribute("src") ?? "");
1888
- if (!dataUrl) {
1889
- image.removeAttribute("src");
1890
- image.removeAttribute("srcset");
1891
- image.style.visibility = "hidden";
1892
- return;
1893
- }
1894
- image.setAttribute("src", dataUrl);
1895
- image.removeAttribute("srcset");
1896
- }),
1897
- );
1898
-
1899
- clone.querySelectorAll("source").forEach((source) => {
1900
- source.removeAttribute("src");
1901
- source.removeAttribute("srcset");
1902
- });
1903
- }
1904
-
1905
- function convertViewportUnits(value, viewportWidth, viewportHeight) {
1906
- return value.replace(
1907
- /(-?\d*\.?\d+)(dvh|svh|lvh|vh|dvw|svw|lvw|vw|vmin|vmax)\b/gi,
1908
- (_, amount, unit) => {
1909
- const number = Number(amount);
1910
- if (!Number.isFinite(number)) return `${amount}${unit}`;
1911
- const normalizedUnit = String(unit).toLowerCase();
1912
- const base =
1913
- normalizedUnit === "vw" ||
1914
- normalizedUnit === "dvw" ||
1915
- normalizedUnit === "svw" ||
1916
- normalizedUnit === "lvw"
1917
- ? viewportWidth
1918
- : normalizedUnit === "vmin"
1919
- ? Math.min(viewportWidth, viewportHeight)
1920
- : normalizedUnit === "vmax"
1921
- ? Math.max(viewportWidth, viewportHeight)
1922
- : viewportHeight;
1923
- return `${(number / 100) * base}px`;
1924
- },
1925
- );
1926
- }
1927
-
1928
- function sanitizeCssForExport(css, viewportWidth, viewportHeight) {
1929
- return convertViewportUnits(css, viewportWidth, viewportHeight)
1930
- .replace(/@font-face\s*{[^}]*}/gi, "")
1931
- .replace(/url\((?!['"]?data:)[^)]+\)/gi, "none");
1932
- }
1933
-
1934
- function sanitizeInlineStyles(clone, viewportWidth, viewportHeight) {
1935
- clone.querySelectorAll("[style]").forEach((node) => {
1936
- node.setAttribute(
1937
- "style",
1938
- sanitizeCssForExport(
1939
- node.getAttribute("style") ?? "",
1940
- viewportWidth,
1941
- viewportHeight,
1942
- ),
1943
- );
1944
- });
1945
- }
1946
-
1947
- function removeCaptureLockStyles(clone) {
1948
- clone.querySelectorAll("[style]").forEach((node) => {
1949
- const style = node.getAttribute("style") ?? "";
1950
- const hasCaptureLock =
1951
- /max-width:\s*none/i.test(style) &&
1952
- /max-height:\s*none/i.test(style) &&
1953
- /transform:\s*none/i.test(style) &&
1954
- /margin:\s*0px/i.test(style);
1955
-
1956
- if (!hasCaptureLock) return;
1957
-
1958
- node.style.removeProperty("width");
1959
- node.style.removeProperty("height");
1960
- node.style.removeProperty("max-width");
1961
- node.style.removeProperty("max-height");
1962
- node.style.removeProperty("margin");
1963
- node.style.removeProperty("transform");
1964
- node.style.removeProperty("inset");
1965
- });
1966
- }
1967
-
1968
- function removeExportUnsafeNodes(clone) {
1969
- clone
1970
- .querySelectorAll("iframe, canvas, video, audio, object, embed")
1971
- .forEach((node) => node.remove());
1972
- clone
1973
- .querySelectorAll("link[rel~='stylesheet'], link[rel~='preload']")
1974
- .forEach((node) => node.remove());
1975
- clone
1976
- .querySelectorAll(
1977
- "#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",
1978
- )
1979
- .forEach((node) => node.remove());
1980
- }
1981
-
1982
- function makeCloneUrlsAbsolute(clone) {
1983
- clone.querySelectorAll("[src]").forEach((node) => {
1984
- node.setAttribute("src", toAbsoluteUrl(node.getAttribute("src")));
1985
- });
1986
- clone.querySelectorAll("[href]").forEach((node) => {
1987
- node.setAttribute("href", toAbsoluteUrl(node.getAttribute("href")));
1988
- });
1989
- clone.querySelectorAll("[srcset]").forEach((node) => {
1990
- const srcset = node
1991
- .getAttribute("srcset")
1992
- .split(",")
1993
- .map((entry) => {
1994
- const parts = entry.trim().split(/\s+/);
1995
- if (!parts[0]) return "";
1996
- parts[0] = toAbsoluteUrl(parts[0]);
1997
- return parts.join(" ");
1998
- })
1999
- .filter(Boolean)
2000
- .join(", ");
2001
- node.setAttribute("srcset", srcset);
2002
- });
2003
- }
2004
-
2005
- function inlineAccessibleStyles(clone, viewportWidth, viewportHeight) {
2006
- const style = clone.ownerDocument.createElement("style");
2007
- let css = "";
2008
-
2009
- Array.from(document.styleSheets).forEach((styleSheet) => {
1920
+ signal.throwIfAborted();
1921
+ let response;
2010
1922
  try {
2011
- css += Array.from(styleSheet.cssRules)
2012
- .map((rule) => rule.cssText)
2013
- .join("\n");
2014
- } catch {
2015
- // Cross-origin stylesheets cannot be exported safely into canvas.
2016
- }
2017
- });
2018
-
2019
- if (!css) return;
2020
- style.textContent = sanitizeCssForExport(
2021
- css,
2022
- viewportWidth,
2023
- viewportHeight,
2024
- );
2025
- clone.querySelector("head")?.appendChild(style);
2026
- }
2027
-
2028
- function markCaptureElements(root, attributeName) {
2029
- return [root, ...Array.from(root.querySelectorAll("*"))].map(
2030
- (element, index) => {
2031
- const previousValue = element.getAttribute(attributeName);
2032
- element.setAttribute(attributeName, String(index));
2033
- return { element, index, previousValue };
2034
- },
2035
- );
2036
- }
2037
-
2038
- function restoreCaptureElementMarks(markedElements, attributeName) {
2039
- markedElements.forEach(({ element, previousValue }) => {
2040
- if (previousValue === null) {
2041
- element.removeAttribute(attributeName);
2042
- return;
2043
- }
2044
- element.setAttribute(attributeName, previousValue);
2045
- });
2046
- }
2047
-
2048
- function removeCaptureElementMarks(clone, attributeName) {
2049
- clone.removeAttribute(attributeName);
2050
- clone
2051
- .querySelectorAll(`[${attributeName}]`)
2052
- .forEach((element) => element.removeAttribute(attributeName));
2053
- }
2054
-
2055
- function pinFixedElementsToViewport(
2056
- clone,
2057
- markedElements,
2058
- attributeName,
2059
- scrollX,
2060
- scrollY,
2061
- viewportWidth,
2062
- viewportHeight,
2063
- ) {
2064
- markedElements.forEach(({ element, index }) => {
2065
- if (window.getComputedStyle(element).position !== "fixed") return;
2066
-
2067
- const clonedElement = clone.querySelector(
2068
- `[${attributeName}="${index}"]`,
2069
- );
2070
- if (!(clonedElement instanceof HTMLElement)) return;
2071
-
2072
- const rect = element.getBoundingClientRect();
2073
- clonedElement.style.position = "absolute";
2074
- clonedElement.style.left = `${rect.left + scrollX}px`;
2075
- clonedElement.style.top = `${rect.top + scrollY}px`;
2076
- clonedElement.style.right = "auto";
2077
- clonedElement.style.bottom = "auto";
2078
- clonedElement.style.boxSizing = "border-box";
2079
- clonedElement.style.width = `${rect.width || viewportWidth}px`;
2080
- clonedElement.style.height = `${rect.height || viewportHeight}px`;
2081
- clonedElement.style.transform = "none";
2082
- });
2083
- }
2084
-
2085
- function syncScrollableElementOffsets(
2086
- clone,
2087
- markedElements,
2088
- attributeName,
2089
- ) {
2090
- markedElements.forEach(({ element, index }) => {
2091
- if (element === document.documentElement || element === document.body) {
2092
- return;
2093
- }
2094
-
2095
- const scrollLeft = element.scrollLeft;
2096
- const scrollTop = element.scrollTop;
2097
- if (!scrollLeft && !scrollTop) return;
2098
-
2099
- const clonedElement = clone.querySelector(
2100
- `[${attributeName}="${index}"]`,
2101
- );
2102
- if (!(clonedElement instanceof HTMLElement)) return;
2103
-
2104
- const children = Array.from(clonedElement.childNodes);
2105
- if (!children.length) return;
2106
-
2107
- const scrollWrapper = clone.ownerDocument.createElement("div");
2108
- const computedStyle = window.getComputedStyle(element);
2109
- scrollWrapper.style.transform = `translate(${-scrollLeft}px, ${-scrollTop}px)`;
2110
- scrollWrapper.style.transformOrigin = "top left";
2111
- scrollWrapper.style.width = `${Math.max(
2112
- element.scrollWidth,
2113
- element.clientWidth,
2114
- )}px`;
2115
- scrollWrapper.style.minHeight = `${Math.max(
2116
- element.scrollHeight,
2117
- element.clientHeight,
2118
- )}px`;
2119
-
2120
- if (computedStyle.display.includes("flex")) {
2121
- scrollWrapper.style.display = computedStyle.display;
2122
- scrollWrapper.style.flexDirection = computedStyle.flexDirection;
2123
- scrollWrapper.style.flexWrap = computedStyle.flexWrap;
2124
- scrollWrapper.style.alignItems = computedStyle.alignItems;
2125
- scrollWrapper.style.justifyContent = computedStyle.justifyContent;
2126
- scrollWrapper.style.gap = computedStyle.gap;
2127
- } else if (computedStyle.display.includes("grid")) {
2128
- scrollWrapper.style.display = computedStyle.display;
2129
- scrollWrapper.style.gridTemplateColumns =
2130
- computedStyle.gridTemplateColumns;
2131
- scrollWrapper.style.gridTemplateRows = computedStyle.gridTemplateRows;
2132
- scrollWrapper.style.gap = computedStyle.gap;
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");
2133
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
+ }
2134
1958
 
2135
- children.forEach((child) => scrollWrapper.appendChild(child));
2136
- clonedElement.appendChild(scrollWrapper);
2137
- });
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);
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);
2138
1994
  }
1995
+ }
1996
+ await Promise.all([...sheets].map(sheet => readSheet(sheet)));
1997
+ return (await Promise.all(fonts)).join("\n");
1998
+ }
2139
1999
 
2140
- async function captureViewportScreenshot() {
2141
- await waitForPaint();
2142
2000
 
2143
- const width = window.innerWidth;
2144
- const height = window.innerHeight;
2145
- const scrollX = window.scrollX;
2146
- const scrollY = window.scrollY;
2147
- const captureWidth = Math.max(width + scrollX, width);
2148
- const documentHeight = Math.max(
2149
- document.documentElement.scrollHeight,
2150
- document.body?.scrollHeight ?? 0,
2151
- height,
2152
- );
2153
- const captureIdAttribute = "data-comty-capture-id";
2154
- const markedElements = markCaptureElements(
2155
- document.documentElement,
2156
- captureIdAttribute,
2157
- );
2158
- let clone;
2001
+ function captureContext() {
2002
+ return { url: location.href, width: innerWidth, height: innerHeight, scrollX, scrollY };
2003
+ }
2159
2004
 
2160
- try {
2161
- clone = document.documentElement.cloneNode(true);
2162
- pinFixedElementsToViewport(
2163
- clone,
2164
- markedElements,
2165
- captureIdAttribute,
2166
- scrollX,
2167
- scrollY,
2168
- width,
2169
- height,
2170
- );
2171
- syncScrollableElementOffsets(clone, markedElements, captureIdAttribute);
2172
- } finally {
2173
- restoreCaptureElementMarks(markedElements, captureIdAttribute);
2174
- }
2175
-
2176
- clone.querySelectorAll("script").forEach((node) => node.remove());
2177
- syncFormValues(document, clone);
2178
- makeCloneUrlsAbsolute(clone);
2179
- await inlineImageResources(clone);
2180
- inlineAccessibleStyles(clone, width, height);
2181
- sanitizeInlineStyles(clone, width, height);
2182
- removeCaptureLockStyles(clone);
2183
- removeExportUnsafeNodes(clone);
2184
- removeCaptureElementMarks(clone, captureIdAttribute);
2185
- clone.style.width = `${captureWidth}px`;
2186
- clone.style.height = `${documentHeight}px`;
2187
- clone.style.margin = "0";
2188
- 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
+ }
2189
2009
 
2190
- const clonedBody = clone.querySelector("body");
2191
- if (clonedBody instanceof HTMLElement) {
2192
- clonedBody.style.width = getComputedStyle(document.body).width;
2193
- clonedBody.style.minHeight = `${documentHeight}px`;
2194
- clonedBody.style.margin = clonedBody.style.margin || "0";
2195
- }
2196
-
2197
- const wrapper = document.createElement("div");
2198
- wrapper.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2199
- wrapper.style.position = "relative";
2200
- wrapper.style.width = `${captureWidth}px`;
2201
- wrapper.style.height = `${documentHeight}px`;
2202
- wrapper.style.overflow = "hidden";
2203
- wrapper.style.transform = `translate(${-scrollX}px, ${-scrollY}px)`;
2204
- wrapper.style.transformOrigin = "top left";
2205
- wrapper.appendChild(clone);
2206
-
2207
- const markup = new XMLSerializer().serializeToString(wrapper);
2208
- const svg = `
2209
- <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
2210
- <foreignObject width="100%" height="100%">${markup}</foreignObject>
2211
- </svg>
2212
- `;
2213
- return {
2214
- blob: new Blob([svg], { type: "image/svg+xml" }),
2215
- width,
2216
- height,
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
+ },
2217
2163
  };
2164
+ const clone = await captureEngine.cloneNode(document.documentElement, options, true);
2165
+ checkContext();
2166
+ frozen = true;
2167
+ const stage = document.createElement("div");
2168
+ stage.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2169
+ stage.style.position = "relative";
2170
+ stage.style.width = `${context.width}px`;
2171
+ stage.style.height = `${context.height}px`;
2172
+ stage.style.overflow = "hidden";
2173
+ // The browser propagates the root (or body's) background to the viewport;
2174
+ // an HTML clone inside SVG does not, especially with collapsed margins.
2175
+ const rootStyle = getComputedStyle(document.documentElement);
2176
+ stage.style.backgroundColor = rootStyle.backgroundColor === "rgba(0, 0, 0, 0)" && rootStyle.backgroundImage === "none"
2177
+ ? getComputedStyle(document.body).backgroundColor : rootStyle.backgroundColor;
2178
+ const documentLayer = document.createElement("div");
2179
+ documentLayer.style.transform = `translate(${-context.scrollX}px, ${-context.scrollY}px)`;
2180
+ documentLayer.style.transformOrigin = "top left";
2181
+ clone.style.width = `${context.width}px`;
2182
+ clone.style.height = `${Math.max(document.documentElement.scrollHeight, context.height)}px`;
2183
+ clone.style.overflow = "visible";
2184
+ clone.style.margin = "0";
2185
+ documentLayer.append(clone);
2186
+ stage.append(documentLayer);
2187
+ for (const item of positioned.sort((a, b) => a.zIndex - b.zIndex)) {
2188
+ const { clone: node, rect } = item;
2189
+ if (rect.bottom <= 0 || rect.right <= 0 || rect.top >= context.height || rect.left >= context.width) continue;
2190
+ Object.assign(node.style, { position: "absolute", left: `${rect.left}px`, top: `${rect.top}px`, right: "auto", bottom: "auto", width: `${rect.width}px`, height: `${rect.height}px`, boxSizing: "border-box", margin: "0", transform: "none" });
2191
+ stage.append(node);
2192
+ }
2193
+ if (annotation) {
2194
+ // Measure the real Comty components in an invisible host, then include
2195
+ // their styled clones in the same font/image embedding as the page.
2196
+ const mounted = annotation();
2197
+ try {
2198
+ cloningAnnotation = true;
2199
+ for (const source of mounted.elements) {
2200
+ const rect = source.getBoundingClientRect();
2201
+ const node = await captureEngine.cloneNode(source, options, true);
2202
+ Object.assign(node.style, { position: "absolute", left: `${rect.left}px`, top: `${rect.top}px`, right: "auto", bottom: "auto", width: `${rect.width}px`, height: `${rect.height}px`, boxSizing: "border-box", margin: "0", transform: "none" });
2203
+ stage.append(node);
2204
+ }
2205
+ } finally {
2206
+ cloningAnnotation = false;
2207
+ mounted.dispose();
2208
+ }
2209
+ }
2210
+ const fonts = await untilAborted(captureFontCSS(sheets, fontFamilies, resources, captureEngine.embedResources, options));
2211
+ if (fonts) {
2212
+ const style = document.createElement("style");
2213
+ style.textContent = fonts;
2214
+ stage.prepend(style);
2215
+ }
2216
+ // Pseudo-element styles are generated by the library and need their URLs embedded too.
2217
+ for (const style of stage.querySelectorAll("style")) {
2218
+ await cooperate();
2219
+ style.textContent = await captureEngine.embedResources(style.textContent, document.baseURI, options);
2220
+ }
2221
+ await untilAborted(captureEngine.embedImages(stage, options));
2222
+ checkContext();
2223
+ await yieldCapture();
2224
+ const svg = await captureEngine.nodeToDataURL(stage, context.width, context.height);
2225
+ const image = new Image();
2226
+ image.src = svg;
2227
+ await untilAborted(image.decode());
2228
+ checkContext();
2229
+ const scale = Math.min(2, devicePixelRatio || 1, 2560 / context.width, 2560 / context.height);
2230
+ const canvas = document.createElement("canvas");
2231
+ canvas.width = Math.round(context.width * scale);
2232
+ canvas.height = Math.round(context.height * scale);
2233
+ const ctx = canvas.getContext("2d");
2234
+ ctx.scale(scale, scale);
2235
+ ctx.drawImage(image, 0, 0);
2236
+ let blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/png")));
2237
+ if (!blob) throw new Error("Screenshot encoding failed");
2238
+ if (blob.size > 2800000) {
2239
+ ctx.globalCompositeOperation = "destination-over";
2240
+ ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue("background-color");
2241
+ ctx.fillRect(0, 0, context.width, context.height);
2242
+ blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", 0.9)));
2243
+ if (!blob) throw new Error("Screenshot encoding failed");
2244
+ }
2245
+ // CSS dimensions keep the eyedropper's coordinate system independent of DPR.
2246
+ return { blob, width: canvas.width, height: canvas.height, cssWidth: context.width, cssHeight: context.height };
2247
+ } finally {
2248
+ clearTimeout(timeout);
2249
+ controller.abort();
2218
2250
  }
2219
-
2220
- return { waitForPaint, waitForCustomElements, captureViewportScreenshot };
2251
+ }
2252
+ return { captureViewportScreenshot };
2221
2253
  }
2222
2254
 
2223
2255
  // Page-local counterpart of the native eyedropper; never captures outside the page.
@@ -2265,7 +2297,9 @@ function createColorMagnifier({ root, isComtyElement, capture }) {
2265
2297
  sampledHex = null;
2266
2298
  if (!enabled) return;
2267
2299
  try {
2268
- const { blob } = await capture();
2300
+ const screenshot = await capture();
2301
+ if (!screenshot) return;
2302
+ const { blob, cssWidth, cssHeight } = screenshot;
2269
2303
  if (!enabled || version !== revision) return;
2270
2304
  const dataUrl = await new Promise((resolve, reject) => {
2271
2305
  const reader = new FileReader();
@@ -2281,7 +2315,11 @@ function createColorMagnifier({ root, isComtyElement, capture }) {
2281
2315
  const probe = document.createElement('canvas').getContext('2d');
2282
2316
  probe.drawImage(next, 0, 0);
2283
2317
  probe.getImageData(0, 0, 1, 1);
2284
- image = next;
2318
+ const scaled = document.createElement('canvas');
2319
+ scaled.width = cssWidth || next.width;
2320
+ scaled.height = cssHeight || next.height;
2321
+ scaled.getContext('2d').drawImage(next, 0, 0, scaled.width, scaled.height);
2322
+ image = scaled;
2285
2323
  paint();
2286
2324
  } catch { /* The CSS-colour fallback remains available without screen permission. */ }
2287
2325
  }
@@ -2328,7 +2366,7 @@ function createCommentThread(context) {
2328
2366
  const {
2329
2367
  root, apiUrl, projectKey, commentStatuses, selectedStatusFilters, placedPins,
2330
2368
  closeBubbles, getCommentStatus, updateCommentStatus, applyStatusDot,
2331
- renderSidepanelComments, applyStatusFilters, loadComments, createThreadItem,
2369
+ renderSidepanelComments, applyStatusFilters, createThreadItem, renderCommentContent,
2332
2370
  isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
2333
2371
  updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
2334
2372
  showAuthPrompt,
@@ -2356,14 +2394,6 @@ function createCommentThread(context) {
2356
2394
  const header = document.createElement("div");
2357
2395
  header.className = "comty-bubble-header";
2358
2396
 
2359
- const title = document.createElement("h2");
2360
- title.className = "comty-bubble-title";
2361
- title.innerText = comment.kind === "agent" ? "Agent task" : "Comment";
2362
- header.appendChild(title);
2363
-
2364
- const headerActions = document.createElement("div");
2365
- headerActions.className = "comty-bubble-header-actions";
2366
-
2367
2397
  const statusMenuButton = document.createElement("button");
2368
2398
  statusMenuButton.className = "comty-status-trigger";
2369
2399
  statusMenuButton.type = "button";
@@ -2402,7 +2432,7 @@ function createCommentThread(context) {
2402
2432
  };
2403
2433
 
2404
2434
  const saveStatus = async (nextStatus, control) => {
2405
- if (!context.canWrite && !comment.local_agent) return false;
2435
+ if (!context.canManage) return false;
2406
2436
  control.disabled = true;
2407
2437
  const savedStatus = await updateCommentStatus(comment, nextStatus);
2408
2438
  control.disabled = false;
@@ -2418,10 +2448,6 @@ function createCommentThread(context) {
2418
2448
  syncStatusTrigger();
2419
2449
  renderSidepanelComments();
2420
2450
  applyStatusFilters();
2421
- if (comment.local_agent && savedStatus === "done") {
2422
- closeBubbles();
2423
- void loadComments({ force: true });
2424
- }
2425
2451
  if (
2426
2452
  selectedStatusFilters.size > 0 &&
2427
2453
  !selectedStatusFilters.has(savedStatusOption.value)
@@ -2431,7 +2457,7 @@ function createCommentThread(context) {
2431
2457
  return true;
2432
2458
  };
2433
2459
 
2434
- statusMenuButton.disabled = comment.local_agent || !context.canWrite;
2460
+ statusMenuButton.disabled = !context.canManage;
2435
2461
  statusMenuButton.onclick = (event) => {
2436
2462
  event.stopPropagation();
2437
2463
  const existingPopover = document.querySelector(
@@ -2443,10 +2469,11 @@ function createCommentThread(context) {
2443
2469
  if (existingPopover) return;
2444
2470
 
2445
2471
  const popover = document.createElement("div");
2446
- popover.className = "comty-popover comty-status-popover";
2472
+ popover.className = "comty-popover comty-comment-popover comty-status-popover";
2447
2473
  const buttonRect = statusMenuButton.getBoundingClientRect();
2448
2474
  popover.style.top = `${buttonRect.bottom + 4}px`;
2449
- popover.style.left = `${buttonRect.right - 140}px`;
2475
+ popover.style.left = `${buttonRect.left}px`;
2476
+ popover.style.transformOrigin = "top left";
2450
2477
 
2451
2478
  for (const status of commentStatuses) {
2452
2479
  const item = document.createElement("button");
@@ -2484,6 +2511,13 @@ function createCommentThread(context) {
2484
2511
  }
2485
2512
 
2486
2513
  root.appendChild(popover);
2514
+ const popoverWidth = popover.offsetWidth;
2515
+ const popoverHeight = popover.offsetHeight;
2516
+ popover.style.left = `${Math.max(8, Math.min(buttonRect.left, innerWidth - popoverWidth - 8))}px`;
2517
+ if (buttonRect.bottom + 4 + popoverHeight > innerHeight - 8) {
2518
+ popover.style.top = `${Math.max(8, buttonRect.top - popoverHeight - 4)}px`;
2519
+ popover.style.transformOrigin = "bottom left";
2520
+ }
2487
2521
 
2488
2522
  const closeStatusPopover = (closeEvent) => {
2489
2523
  if (
@@ -2500,9 +2534,7 @@ function createCommentThread(context) {
2500
2534
  );
2501
2535
  };
2502
2536
 
2503
- headerActions.appendChild(statusMenuButton);
2504
- headerActions.appendChild(closeButton);
2505
- header.appendChild(headerActions);
2537
+ header.append(statusMenuButton, closeButton);
2506
2538
  bubble.appendChild(header);
2507
2539
 
2508
2540
  const thread = document.createElement("div");
@@ -2521,7 +2553,7 @@ function createCommentThread(context) {
2521
2553
 
2522
2554
  const popover = document.createElement("div");
2523
2555
  popover.className =
2524
- "comty-popover comty-entry-actions-popover";
2556
+ "comty-popover comty-comment-popover comty-entry-actions-popover";
2525
2557
 
2526
2558
  const editItem = document.createElement("button");
2527
2559
  editItem.className = "comty-popover-item";
@@ -2574,7 +2606,7 @@ function createCommentThread(context) {
2574
2606
  );
2575
2607
  };
2576
2608
 
2577
- const mainItem = createThreadItem(comment, { showMore: !comment.local_agent && isOwnCommentEntry(comment) });
2609
+ const mainItem = createThreadItem(comment, { showMore: isOwnCommentEntry(comment) });
2578
2610
 
2579
2611
  const startEditingEntry = (entry, item, persist) => {
2580
2612
  if (!isOwnCommentEntry(entry)) return;
@@ -2607,7 +2639,7 @@ function createCommentThread(context) {
2607
2639
 
2608
2640
  const previousContent = entry.content;
2609
2641
  entry.content = nextContent;
2610
- item.content.innerText = nextContent;
2642
+ renderCommentContent(item.content, nextContent);
2611
2643
  editor.disabled = true;
2612
2644
  editor.replaceWith(item.content);
2613
2645
  renderSidepanelComments();
@@ -2615,11 +2647,11 @@ function createCommentThread(context) {
2615
2647
  const savedContent = await persist(nextContent);
2616
2648
  if (!savedContent) {
2617
2649
  entry.content = previousContent;
2618
- item.content.innerText = previousContent || "";
2650
+ renderCommentContent(item.content, previousContent || "");
2619
2651
  renderSidepanelComments();
2620
2652
  return;
2621
2653
  }
2622
- item.content.innerText = savedContent;
2654
+ renderCommentContent(item.content, savedContent);
2623
2655
  renderSidepanelComments();
2624
2656
  };
2625
2657
 
@@ -2721,13 +2753,15 @@ function createCommentThread(context) {
2721
2753
  const replyForm = document.createElement("div");
2722
2754
  replyForm.className = "comty-reply-form";
2723
2755
 
2724
- if (context.canWrite && !comment.local_agent) {
2756
+ if (context.canWrite) {
2725
2757
  replyForm.appendChild(createCommentAvatar(context.currentUser || { name: "You" }));
2726
2758
  const replyField = document.createElement("div");
2727
2759
  replyField.className = "comty-reply-field";
2728
- const replyInput = document.createElement("input");
2760
+ const replyInput = document.createElement("textarea");
2729
2761
  replyInput.className = "comty-reply-input";
2730
2762
  replyInput.placeholder = "Reply";
2763
+ replyInput.rows = 1;
2764
+ replyInput.setAttribute("aria-label", "Reply");
2731
2765
 
2732
2766
  const replyButton = document.createElement("button");
2733
2767
  replyButton.className = "comty-reply-button";
@@ -2761,6 +2795,7 @@ function createCommentThread(context) {
2761
2795
  comment.replies = replies;
2762
2796
  appendReply(data.reply);
2763
2797
  replyInput.value = "";
2798
+ syncReplySize();
2764
2799
  requestAnimationFrame(() => {
2765
2800
  threadList.scrollTop = threadList.scrollHeight;
2766
2801
  });
@@ -2774,12 +2809,28 @@ function createCommentThread(context) {
2774
2809
  }
2775
2810
  };
2776
2811
 
2812
+ const syncReplySize = () => {
2813
+ const currentHeight = replyInput.getBoundingClientRect().height;
2814
+ replyInput.style.transition = "none";
2815
+ const lineHeight = parseFloat(getComputedStyle(replyInput).lineHeight);
2816
+ replyInput.style.height = `${lineHeight}px`;
2817
+ const contentHeight = replyInput.scrollHeight;
2818
+ replyInput.style.height = `${currentHeight}px`;
2819
+ replyInput.getBoundingClientRect();
2820
+ replyInput.style.transition = "";
2821
+ const nextHeight = Math.min(contentHeight, parseFloat(getComputedStyle(replyInput).maxHeight));
2822
+ replyInput.style.height = `${nextHeight}px`;
2823
+ if (contentHeight <= lineHeight + 1) replyInput.scrollTop = 0;
2824
+ replyField.classList.toggle("comty-reply-expanded", contentHeight > lineHeight + 1);
2825
+ };
2826
+
2777
2827
  replyInput.oninput = () => {
2828
+ syncReplySize();
2778
2829
  replyButton.disabled = replyInput.value.trim().length === 0;
2779
2830
  };
2780
2831
  replyInput.onkeydown = (event) => {
2781
2832
  event.stopPropagation();
2782
- if (event.key === "Enter" && !event.shiftKey) {
2833
+ if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
2783
2834
  event.preventDefault();
2784
2835
  void submitReply();
2785
2836
  }
@@ -2792,20 +2843,20 @@ function createCommentThread(context) {
2792
2843
  replyField.appendChild(replyInput);
2793
2844
  replyField.appendChild(replyButton);
2794
2845
  replyForm.appendChild(replyField);
2795
- } else if (!comment.local_agent) {
2846
+ } else {
2796
2847
  replyForm.classList.add("comty-reply-form-guest");
2797
2848
  const signInButton = document.createElement("button");
2798
2849
  signInButton.className = "comty-sign-in-reply";
2799
2850
  signInButton.type = "button";
2800
- signInButton.innerText = "Sign in to reply";
2851
+ signInButton.innerText = context.allowGuestComments ? "Leave a reply" : "Sign in to reply";
2801
2852
  signInButton.onclick = (event) => {
2802
2853
  event.stopPropagation();
2803
- showAuthPrompt();
2854
+ showAuthPrompt(false, () => openCommentThread(comment, pin, pinStatusDot, x, y, removeHoverBubble));
2804
2855
  };
2805
2856
  replyForm.appendChild(signInButton);
2806
2857
  }
2807
2858
 
2808
- if (!comment.local_agent) thread.appendChild(replyForm);
2859
+ thread.appendChild(replyForm);
2809
2860
  bubble.appendChild(thread);
2810
2861
  root.appendChild(bubble);
2811
2862
 
@@ -3042,17 +3093,7 @@ const instanceKey = Symbol.for("comty.sdk.instance");
3042
3093
  /** Initialize once per document; safe to import and call during server rendering. */
3043
3094
  function initComty(options = {}) {
3044
3095
  if (typeof window === "undefined" || typeof document === "undefined") return Promise.resolve();
3045
- const isLocalSite = ["localhost", "127.0.0.1", "[::1]"].includes(window.location.hostname);
3046
- const agent = options.agent;
3047
- if (agent && (!isLocalSite || typeof agent.token !== "string" || !/^[a-f0-9]{64}$/.test(agent.token))) {
3048
- return Promise.reject(new TypeError("Comty: invalid local companion configuration"));
3049
- }
3050
- if (agent) {
3051
- try {
3052
- if (new URL(agent.url).origin !== window.location.origin) throw new Error();
3053
- } catch { return Promise.reject(new TypeError("Comty: companion must share the page origin")); }
3054
- }
3055
- const projectKey = options.projectKey || (agent ? "local" : undefined);
3096
+ const projectKey = options.projectKey;
3056
3097
  const apiUrl = options.apiUrl || "http://localhost:4000";
3057
3098
  if (typeof projectKey !== "string" || !projectKey.trim()) {
3058
3099
  return Promise.reject(new TypeError("Comty: projectKey is required"));
@@ -3064,14 +3105,12 @@ function initComty(options = {}) {
3064
3105
  if (!["http:", "https:"].includes(parsedApi.protocol)) {
3065
3106
  return Promise.reject(new TypeError("Comty: apiUrl must be an absolute HTTP(S) URL"));
3066
3107
  }
3067
- const configuration = JSON.stringify([projectKey, apiUrl, agent || null]);
3108
+ const configuration = JSON.stringify([projectKey, apiUrl]);
3068
3109
  const existing = window[instanceKey];
3069
3110
  if (existing) {
3070
3111
  return existing.configuration === configuration ? existing.ready
3071
3112
  : Promise.reject(new Error("Comty: already initialized with different options"));
3072
3113
  }
3073
- const agentBridgeUrl = agent?.url;
3074
- const localOnly = Boolean(agent && !options.projectKey);
3075
3114
  const ready = new Promise((resolve, reject) => {
3076
3115
  const mount = () => {
3077
3116
  try { resolve(mountComty()); } catch (error) { reject(error); }
@@ -3085,19 +3124,10 @@ function initComty(options = {}) {
3085
3124
 
3086
3125
  function mountComty() {
3087
3126
  let projectId = null;
3088
- let localTasks = [];
3089
- let localApprovals = [];
3090
- let agentPanelOpen = false;
3091
- let agentPanelSignature = "";
3092
- let agentPanelTransition = 0;
3093
- let agentConnectionError = "";
3094
- let agentActionError = "";
3095
- const agentPendingActions = new Set();
3096
3127
  const session = {
3097
3128
  mode: "off",
3098
- get comments() { return this.mode === "browse" || this.mode === "edit" || this.mode === "agent"; },
3099
- get edit() { return this.mode === "edit" || this.mode === "agent"; },
3100
- get agent() { return this.mode === "agent"; },
3129
+ get comments() { return this.mode === "browse" || this.mode === "edit"; },
3130
+ get edit() { return this.mode === "edit"; },
3101
3131
  get inspect() { return this.mode === "inspect"; },
3102
3132
  };
3103
3133
  const selectedStatusFilters = new Set();
@@ -3111,21 +3141,22 @@ function initComty(options = {}) {
3111
3141
  const observedCommentTargets = new WeakSet();
3112
3142
  let sidepanelOpen = false;
3113
3143
  let canWrite = false;
3144
+ let canManage = false;
3145
+ let allowGuestComments = false;
3114
3146
  let loginUrl = "";
3115
3147
  let currentUser = null;
3116
3148
  let authStatus = "";
3117
3149
  let didCompleteSignIn = false;
3118
3150
  const tokenStorageKey = `comty:${projectKey}:token`;
3151
+ const authReturnStorageKey = `comty:${projectKey}:auth-return`;
3119
3152
  let sdkToken = "";
3120
3153
  const activeHoverCommentIds = new Set();
3121
- /** @type {null | 'edit' | 'browse' | 'agent'} */
3154
+ /** @type {null | 'edit' | 'browse'} */
3122
3155
  let sessionEntry = null;
3123
3156
  const placedPins = new Map();
3124
- const workingOutlines = new Map();
3125
3157
  const placementDiagnostics = new Map();
3126
3158
  let inspectedElement = null;
3127
3159
  let selectedElement = null;
3128
- let inspectExpanded = false;
3129
3160
  let measureAlt = false;
3130
3161
  let inspectDrag = null;
3131
3162
  let measuredRegion = null;
@@ -3168,7 +3199,6 @@ function initComty(options = {}) {
3168
3199
  const root = document.createElement("div");
3169
3200
  root.id = "comty-root";
3170
3201
  document.body.appendChild(root);
3171
- if (agent) document.documentElement.classList.add("comty-agent-enabled");
3172
3202
 
3173
3203
  const style = document.createElement("style");
3174
3204
  style.innerHTML = getComtyStyle();
@@ -3216,40 +3246,20 @@ function initComty(options = {}) {
3216
3246
  const inspectorBtn = toolButton("Inspect · Shift I", '<path d="M8 3H5a2 2 0 0 0-2 2v3m13-5h3a2 2 0 0 1 2 2v3M3 16v3a2 2 0 0 0 2 2h3m13-5v3a2 2 0 0 1-2 2h-3"/><path d="m9 9 8 4-4 1-1 4Z"/>', () => setSession("inspect"));
3217
3247
  const btn = toolButton("Comment · Shift C", '<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.4 8.4 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6A8.4 8.4 0 0 1 12.5 3h.5a8.5 8.5 0 0 1 8 8v.5Z"/>', () => {
3218
3248
  if (!canWrite) showAuthPrompt();
3219
- else if (session.agent || session.inspect || !isSessionActive()) startEditSession();
3249
+ else if (session.inspect || !isSessionActive()) startEditSession();
3220
3250
  else if (!session.edit) setSession("edit", { entry: sessionEntry });
3221
3251
  });
3222
3252
  btn.id = "comty-toggle";
3223
- const agentBtn = isLocalSite
3224
- ? toolButton("Agent task · localhost only", '<path d="M8 7 3 12l5 5m8-10 5 5-5 5M14 4l-4 16"/>', () => {
3225
- startAgentSession();
3226
- })
3227
- : null;
3228
3253
  const divider = document.createElement("span");
3229
3254
  divider.className = "comty-tool-divider";
3230
3255
  toolbar.appendChild(divider);
3231
3256
  const commentTools = document.createElement("div");
3232
3257
  commentTools.className = "comty-tool-context";
3233
3258
  toolbar.appendChild(commentTools);
3234
- const agentTools = agentBtn ? document.createElement("div") : null;
3235
- if (agentTools) {
3236
- agentTools.className = "comty-tool-context";
3237
- agentTools.hidden = true;
3238
- toolbar.appendChild(agentTools);
3239
- }
3240
- const agentHistoryBtn = agentTools ? toolButton("Local agent history", '<path d="M3 12a9 9 0 1 0 3-6.7M3 4v4h4"/><path d="M12 7v5l3 2"/>', () => {
3241
- if (!agent) { showAgentBridgeError(); return; }
3242
- agentPanelOpen = !agentPanelOpen;
3243
- renderAgentPanel();
3244
- }) : null;
3245
- if (agentHistoryBtn) {
3246
- agentHistoryBtn.setAttribute("aria-expanded", "false");
3247
- agentTools.appendChild(agentHistoryBtn);
3248
- }
3249
3259
  const primaryTools = document.createElement("div");
3250
3260
  primaryTools.className = "comty-tool-primary";
3251
3261
  toolbar.insertBefore(primaryTools, browseBtn);
3252
- for (const button of [browseBtn, inspectorBtn, btn, agentBtn].filter(Boolean)) primaryTools.appendChild(button);
3262
+ for (const button of [browseBtn, inspectorBtn, btn]) primaryTools.appendChild(button);
3253
3263
  const inspectorTools = document.createElement("div");
3254
3264
  inspectorTools.className = "comty-tool-context";
3255
3265
  inspectorTools.hidden = true;
@@ -3828,6 +3838,21 @@ function initComty(options = {}) {
3828
3838
  const copyIcon = icon('<rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/>');
3829
3839
  const checkIcon = icon('<path d="m5 12 4 4 10-10"/>');
3830
3840
  const copyFeedbackTimers = new WeakMap();
3841
+ async function copyPropertyValue(button, feedback) {
3842
+ clearTimeout(copyFeedbackTimers.get(button));
3843
+ try {
3844
+ await navigator.clipboard.writeText(button.dataset.copy);
3845
+ if (!button.isConnected) return;
3846
+ button.dataset.copied = "";
3847
+ feedback.textContent = "Copied";
3848
+ clearTimeout(copyFeedbackTimers.get(button));
3849
+ copyFeedbackTimers.set(button, setTimeout(() => { delete button.dataset.copied; }, 1200));
3850
+ } catch {
3851
+ delete button.dataset.copied;
3852
+ if (button.isConnected) feedback.textContent = "Clipboard unavailable — select the value to copy";
3853
+ }
3854
+ }
3855
+
3831
3856
  function propertyRow(label, value, display = value) {
3832
3857
  const unavailable = value === "—";
3833
3858
  return `<div class="comty-property-row"><span>${label}</span><button type="button" data-copy="${escapeInspectorText(value)}" aria-label="Copy ${label}: ${escapeInspectorText(display)}" ${unavailable ? "disabled" : ""}><span>${escapeInspectorText(display)}</span>${unavailable ? "" : `<i class="comty-property-copy-icon" aria-hidden="true">${copyIcon}${checkIcon}</i>`}</button></div>`;
@@ -3854,7 +3879,7 @@ function initComty(options = {}) {
3854
3879
  propertyPanel.setAttribute("aria-label", propertyKind === "color" ? "Recent colours" : "Recent typography");
3855
3880
  const recent = propertyKind === "color" ? recentColors : recentFonts;
3856
3881
  const details = propertyKind === "color" && expanded
3857
- ? `<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 ? `var(${propertySelection.token})` : "—")}</div></div>`
3882
+ ? `<div class="comty-property-detail"><div class="comty-property-rows">${propertyRow("HEX", propertySelection.hex)}${propertyRow("RGB", propertySelection.rgb)}${propertyRow("HSL", propertySelection.hsl)}${propertyRow("OKLCH", propertySelection.oklch)}${propertyRow("Token", propertySelection.token ? propertySelection.token.replace(/^--/, "") : "—", propertySelection.token || "—")}</div></div>`
3858
3883
  : propertyKind === "font" && expanded
3859
3884
  ? `<div class="comty-property-detail"><div class="comty-property-rows">${propertyRow("Family", propertySelection.family)}${propertyRow("Size", propertySelection.size)}${propertyRow("Weight", propertySelection.weight)}${propertyRow("Leading", propertySelection.leading)}${propertyRow("Tracking", propertySelection.tracking)}${propertyRow("Colour", propertySelection.color?.hex || "—")}${propertyRow("CSS", propertySelection.css, "All of the above")}</div></div>`
3860
3885
  : "";
@@ -3970,18 +3995,7 @@ function initComty(options = {}) {
3970
3995
  renderPropertyPanel(true);
3971
3996
  propertyPanel.querySelector(`button[data-recent="${button.dataset.recent}"]`)?.focus({ preventScroll: true });
3972
3997
  } else if (button.dataset.copy != null && button.dataset.copy !== "—") {
3973
- clearTimeout(copyFeedbackTimers.get(button));
3974
- try {
3975
- await navigator.clipboard.writeText(button.dataset.copy);
3976
- if (!button.isConnected || propertyPanel.hidden) return;
3977
- button.dataset.copied = "";
3978
- propertyPanel.querySelector(".comty-property-feedback").textContent = `${button.getAttribute("aria-label").slice(5)} copied`;
3979
- clearTimeout(copyFeedbackTimers.get(button));
3980
- copyFeedbackTimers.set(button, setTimeout(() => { delete button.dataset.copied; }, 1200));
3981
- } catch {
3982
- delete button.dataset.copied;
3983
- if (button.isConnected && !propertyPanel.hidden) propertyPanel.querySelector(".comty-property-feedback").textContent = "Clipboard unavailable";
3984
- }
3998
+ await copyPropertyValue(button, propertyPanel.querySelector(".comty-property-feedback"));
3985
3999
  }
3986
4000
  };
3987
4001
 
@@ -4098,50 +4112,6 @@ function initComty(options = {}) {
4098
4112
  let toolbarMotionFrame = 0;
4099
4113
  let toolbarAnimations = [];
4100
4114
  const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
4101
- let workingIndicatorTimer = 0;
4102
- function scheduleWorkingIndicators() {
4103
- if (workingIndicatorTimer || reducedMotion.matches || document.hidden) return;
4104
- workingIndicatorTimer = setTimeout(() => {
4105
- workingIndicatorTimer = 0;
4106
- if (!root.isConnected) return;
4107
- const indicators = root.querySelectorAll('.comty-working-indicator');
4108
- if (!indicators.length) return;
4109
- for (const indicator of indicators) {
4110
- const previous = Number(indicator.dataset.activeDots.split(',')[0]);
4111
- const first = (previous + 1 + Math.floor(Math.random() * 8)) % 9;
4112
- const second = (first + 1 + Math.floor(Math.random() * 8)) % 9;
4113
- indicator.dataset.activeDots = `${first},${second}`;
4114
- [...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === first || index === second));
4115
- }
4116
- scheduleWorkingIndicators();
4117
- }, 600);
4118
- }
4119
- function createWorkingIndicator() {
4120
- const indicator = document.createElement('span');
4121
- indicator.className = 'comty-working-indicator';
4122
- indicator.setAttribute('aria-hidden', 'true');
4123
- indicator.dataset.activeDots = '3,5';
4124
- for (let index = 0; index < 9; index++) {
4125
- const dot = document.createElement('i');
4126
- dot.toggleAttribute('data-active', index === 3 || index === 5);
4127
- indicator.appendChild(dot);
4128
- }
4129
- scheduleWorkingIndicators();
4130
- return indicator;
4131
- }
4132
- function resetWorkingIndicators() {
4133
- clearTimeout(workingIndicatorTimer);
4134
- workingIndicatorTimer = 0;
4135
- if (reducedMotion.matches) {
4136
- for (const indicator of root.querySelectorAll('.comty-working-indicator')) {
4137
- indicator.dataset.activeDots = '3,5';
4138
- [...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === 3 || index === 5));
4139
- }
4140
- }
4141
- if (root.querySelector('.comty-working-indicator')) scheduleWorkingIndicators();
4142
- }
4143
- reducedMotion.addEventListener('change', resetWorkingIndicators);
4144
- document.addEventListener('visibilitychange', resetWorkingIndicators);
4145
4115
  const clampToolbar = (value, size, viewport) => Math.max(8, Math.min(value, viewport - size - 8));
4146
4116
  function rememberToolbarPosition(left, top) {
4147
4117
  const width = toolbar.offsetWidth, height = toolbar.offsetHeight;
@@ -4254,7 +4224,7 @@ function initComty(options = {}) {
4254
4224
 
4255
4225
  function prepareToolbarMotion() {
4256
4226
  if (toolbarMotionFrame) return;
4257
- const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, agentTools, inspectorTools].filter(Boolean);
4227
+ const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools].filter(Boolean);
4258
4228
  // Move each group's background with its icons, including when interrupted.
4259
4229
  const before = new Map(nodes.filter(node => !node.hidden).map(node => [node, {
4260
4230
  rect: node.getBoundingClientRect(), opacity: getComputedStyle(node).opacity,
@@ -4497,7 +4467,7 @@ function initComty(options = {}) {
4497
4467
  return;
4498
4468
  }
4499
4469
 
4500
- if ((session.edit && mode === "browse") || (session.agent && mode === "edit")) {
4470
+ if (session.edit && mode === "browse") {
4501
4471
  closeBubbles();
4502
4472
  }
4503
4473
  const hadComments = session.comments;
@@ -4511,7 +4481,6 @@ function initComty(options = {}) {
4511
4481
  } else {
4512
4482
  hideInspector();
4513
4483
  }
4514
- if (session.agent && sidepanelOpen) setSidepanelOpen(false);
4515
4484
  syncToolbar();
4516
4485
  ensureLiveTracking();
4517
4486
  if (renderComments || !hadComments) renderCachedComments();
@@ -4523,11 +4492,8 @@ function initComty(options = {}) {
4523
4492
  : headers;
4524
4493
  }
4525
4494
 
4526
- function showAuthPrompt(forbidden = false) {
4527
- if (localOnly) {
4528
- showAgentBridgeError("Shared comments need a Comty project key. Use the Agent tool for local tasks without signing in.");
4529
- return;
4530
- }
4495
+ function showAuthPrompt(forbidden = false, onReady = startEditSession) {
4496
+ const guestAllowed = allowGuestComments;
4531
4497
  document.querySelector(".comty-auth-backdrop")?.remove();
4532
4498
  const opener = document.activeElement;
4533
4499
  const backdrop = document.createElement("div");
@@ -4538,12 +4504,23 @@ function initComty(options = {}) {
4538
4504
  <span class="comty-auth-icon">${icon('<path d="M21 11.5a8.4 8.4 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7a8.5 8.5 0 1 1 16.1-3.8Z"/>')}</span>
4539
4505
  <button type="button" class="comty-auth-close" data-comty-close aria-label="Close sign-in dialog">${icon('<path d="m6 6 12 12M6 18 18 6"/>')}</button>
4540
4506
  </div>
4541
- <h2 id="comty-auth-title">${forbidden ? "Project access required" : "Sign in to comment"}</h2>
4542
- <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>
4543
- <div class="comty-auth-actions">
4544
- <button type="button" data-comty-close>${forbidden ? "Close" : "Cancel"}</button>
4545
- ${forbidden ? "" : '<button type="button" class="comty-auth-primary" data-comty-login>Continue to sign in</button>'}
4546
- </div>
4507
+ <h2 id="comty-auth-title">${guestAllowed ? "Leave a comment" : forbidden ? "Project access required" : "Sign in to comment"}</h2>
4508
+ <p id="comty-auth-description">${guestAllowed ? "Continue with your name or sign in with Google." : forbidden ? "This account is not a member of this project. Ask the project owner for an invite." : "Sign in to share feedback with your team and keep the conversation in one place."}</p>
4509
+ ${guestAllowed ? `
4510
+ <form class="comty-guest-form">
4511
+ <label for="comty-guest-name">Your name</label>
4512
+ <input id="comty-guest-name" name="name" autocomplete="name" placeholder="Your name" maxlength="80" required aria-describedby="comty-guest-error" />
4513
+ <p id="comty-guest-error" role="alert" hidden></p>
4514
+ <div class="comty-auth-actions comty-guest-actions">
4515
+ <button type="submit" class="comty-auth-primary" data-comty-guest>Continue</button>
4516
+ <span class="comty-auth-or">or</span>
4517
+ <button type="button" data-comty-login>Continue with Google</button>
4518
+ </div>
4519
+ </form>` : `
4520
+ <div class="comty-auth-actions">
4521
+ <button type="button" data-comty-close>${forbidden ? "Close" : "Cancel"}</button>
4522
+ ${forbidden ? "" : '<button type="button" class="comty-auth-primary" data-comty-login>Continue to sign in</button>'}
4523
+ </div>`}
4547
4524
  </div>`;
4548
4525
  const close = () => {
4549
4526
  backdrop.remove();
@@ -4554,11 +4531,12 @@ function initComty(options = {}) {
4554
4531
  event.stopPropagation();
4555
4532
  if (event.key === "Escape") { event.preventDefault(); close(); }
4556
4533
  if (event.key === "Tab") {
4557
- const buttons = [...backdrop.querySelectorAll("button:not(:disabled)")];
4534
+ const buttons = [...backdrop.querySelectorAll("button:not(:disabled), input:not(:disabled)")];
4558
4535
  if (!buttons.length) { event.preventDefault(); return; }
4559
- const first = buttons[0], last = buttons.at(-1);
4560
- if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
4561
- else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
4536
+ event.preventDefault();
4537
+ const index = buttons.indexOf(document.activeElement);
4538
+ const next = (index + (event.shiftKey ? -1 : 1) + buttons.length) % buttons.length;
4539
+ buttons[next].focus();
4562
4540
  }
4563
4541
  });
4564
4542
  const loginButton = backdrop.querySelector("[data-comty-login]");
@@ -4566,15 +4544,62 @@ function initComty(options = {}) {
4566
4544
  loginButton.onclick = () => {
4567
4545
  loginButton.disabled = true;
4568
4546
  loginButton.innerText = "Connecting…";
4569
- backdrop.querySelectorAll("[data-comty-close]").forEach(button => { button.disabled = true; });
4570
- setTimeout(() => window.location.assign(loginUrl), 50);
4547
+ backdrop.querySelectorAll("button, input").forEach(control => { control.disabled = true; });
4548
+ try {
4549
+ sessionStorage.setItem(authReturnStorageKey, JSON.stringify({
4550
+ mode: onReady === startEditSession || session.mode === "off" ? "edit" : session.mode,
4551
+ entry: sessionEntry,
4552
+ sidepanelOpen,
4553
+ }));
4554
+ } catch { /* Resume in comment mode if browser storage is unavailable. */ }
4555
+ const destination = new URL("/api/auth/sign-in/google", apiUrl);
4556
+ destination.searchParams.set("callbackURL", loginUrl);
4557
+ setTimeout(() => window.location.assign(guestAllowed ? destination.toString() : loginUrl), 50);
4571
4558
  };
4572
4559
  }
4560
+ const guestForm = backdrop.querySelector(".comty-guest-form");
4561
+ if (guestForm) guestForm.onsubmit = async event => {
4562
+ event.preventDefault();
4563
+ const nameInput = guestForm.querySelector("input");
4564
+ const error = guestForm.querySelector("[role=alert]");
4565
+ const name = nameInput.value.trim();
4566
+ if (!name) { nameInput.focus(); return; }
4567
+ const controls = [...backdrop.querySelectorAll("button, input")];
4568
+ controls.forEach(control => { control.disabled = true; });
4569
+ error.hidden = true;
4570
+ const continueButton = guestForm.querySelector("[data-comty-guest]");
4571
+ continueButton.textContent = "Connecting…";
4572
+ try {
4573
+ const response = await fetch(`${apiUrl}/api/sdk/guest`, {
4574
+ method: "POST",
4575
+ headers: { "Content-Type": "application/json" },
4576
+ body: JSON.stringify({ projectKey, name }),
4577
+ });
4578
+ const data = await response.json();
4579
+ if (!response.ok) throw new Error(data.error || "Unable to continue. Please try again.");
4580
+ sdkToken = data.token;
4581
+ try { sessionStorage.setItem(tokenStorageKey, sdkToken); } catch { /* Keep the session in memory when browser storage is unavailable. */ }
4582
+ currentUser = data.user;
4583
+ canWrite = true;
4584
+ canManage = false;
4585
+ if (backdrop.isConnected) {
4586
+ close();
4587
+ onReady();
4588
+ }
4589
+ } catch (failure) {
4590
+ if (!backdrop.isConnected) return;
4591
+ error.textContent = failure.message || "Unable to connect. Please try again.";
4592
+ error.hidden = false;
4593
+ controls.forEach(control => { control.disabled = false; });
4594
+ continueButton.textContent = "Continue";
4595
+ nameInput.focus();
4596
+ }
4597
+ };
4573
4598
  backdrop.onclick = (event) => {
4574
4599
  if (event.target === backdrop) close();
4575
4600
  };
4576
4601
  root.appendChild(backdrop);
4577
- (loginButton || backdrop.querySelector("[data-comty-close]")).focus({ preventScroll: true });
4602
+ (backdrop.querySelector("#comty-guest-name") || loginButton || backdrop.querySelector("[data-comty-close]")).focus({ preventScroll: true });
4578
4603
  }
4579
4604
 
4580
4605
  function formatBoxValue(value) {
@@ -4589,17 +4614,14 @@ function initComty(options = {}) {
4589
4614
  }
4590
4615
 
4591
4616
  function syncToolbar() {
4592
- if (commentTools.hidden !== (session.inspect || session.agent) || inspectorTools.hidden !== !session.inspect || (agentTools && agentTools.hidden !== !session.agent)) prepareToolbarMotion();
4617
+ if (commentTools.hidden !== session.inspect || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
4593
4618
  root.classList.toggle("comty-active", session.comments || session.edit || sidepanelOpen || session.inspect);
4594
4619
  browseBtn.setAttribute("aria-pressed", String(!session.edit && !session.inspect));
4595
4620
  inspectorBtn.setAttribute("aria-pressed", String(session.inspect));
4596
- btn.setAttribute("aria-pressed", String(session.edit && !session.agent));
4597
- agentBtn?.setAttribute("aria-pressed", String(session.edit && session.agent));
4598
- commentTools.hidden = session.inspect || session.agent;
4599
- if (agentTools) agentTools.hidden = !session.agent;
4621
+ btn.setAttribute("aria-pressed", String(session.edit));
4622
+ commentTools.hidden = session.inspect;
4600
4623
  inspectorTools.hidden = !session.inspect;
4601
4624
  syncInspectorTools();
4602
- renderAgentPanel();
4603
4625
  }
4604
4626
 
4605
4627
  const inspectionLayer = document.createElement("div");
@@ -4774,7 +4796,8 @@ function initComty(options = {}) {
4774
4796
  add("background", css.backgroundColor, (tokens.background.length > 0 || !["rgba(0, 0, 0, 0)", "rgb(255, 255, 255)"].includes(css.backgroundColor)));
4775
4797
  add("color", css.color);
4776
4798
  if (getElementOwnTextSignature(el)) {
4777
- add("font", `${css.fontSize} / ${css.fontWeight}`);
4799
+ add("font-size", css.fontSize);
4800
+ add("font-weight", css.fontWeight);
4778
4801
  add("font-family", css.fontFamily);
4779
4802
  add("line-height", css.lineHeight);
4780
4803
  add("letter-spacing", css.letterSpacing, css.letterSpacing !== "normal");
@@ -4958,7 +4981,7 @@ function initComty(options = {}) {
4958
4981
  height: draftBubble.offsetHeight,
4959
4982
  };
4960
4983
  const pinBounds = draftPin?.getBoundingClientRect();
4961
- const signature = JSON.stringify([selector, rect, hoverRect, rows, !!selected, inspectExpanded, measureAlt, draftBounds, pinBounds, spacing]);
4984
+ const signature = JSON.stringify([selector, rect, hoverRect, rows, !!selected, measureAlt, draftBounds, pinBounds, spacing, innerWidth, innerHeight]);
4962
4985
  if (signature === inspectionSignature) return;
4963
4986
  inspectionSignature = signature;
4964
4987
  inspectionLayer.replaceChildren();
@@ -4984,36 +5007,34 @@ function initComty(options = {}) {
4984
5007
  const panel = document.createElement("div");
4985
5008
  panel.className = "comty-inspector";
4986
5009
  panel.setAttribute("aria-label", "Element properties");
4987
- if (selected) panel.dataset.selected = "";
4988
- const copyable = (value) => `<button type="button" title="Copy ${escapeInspectorText(value)}" data-copy="${escapeInspectorText(value)}">${escapeInspectorText(value)}</button>`;
4989
- const featured = new Set(["background", "color", "font", "font-family"]);
4990
- const visibleRows = inspectExpanded ? rows : selected ? [...rows.filter(([key]) => featured.has(key)), ...rows.filter(([key]) => !featured.has(key))].slice(0, 6) : rows.slice(0, 6);
4991
- panel.innerHTML = `<div class="comty-inspector-head">${copyable(selector)}${copyable(`${formatBoxValue(rect.width)} × ${formatBoxValue(rect.height)}`)}</div><div class="comty-inspector-details">${visibleRows.map(([key, value, tokens]) => {
4992
- const picker = key === "background" || key === "color" ? "color" : key === "font" || key === "font-family" ? "font" : "";
4993
- const valueButton = picker && selected
4994
- ? `<button type="button" class="comty-inspector-picker" data-picker="${picker}" data-property="${key}" aria-label="Open ${picker === "color" ? key + " colour" : "typography"} details">${picker === "color" ? '<span class="comty-inspector-swatch" aria-hidden="true"></span>' : ""}${escapeInspectorText(value)}</button>`
4995
- : copyable(value);
4996
- return `<div class="comty-inspector-row"><span>${key}</span><div class="comty-inspector-value">${tokens?.length ? `<div class="comty-inspector-tokens">${tokens.map(token => copyable(token)).join("")}</div>` : ""}${valueButton}</div></div>`;
4997
- }).join("")}</div>${rows.length > 6 && selected ? `<button type="button" class="comty-inspector-expand">${inspectExpanded ? "Show less" : `Show ${rows.length - 6} more`}</button>` : ""}`;
4998
- for (const button of panel.querySelectorAll('.comty-inspector-picker[data-picker="color"]')) {
4999
- button.querySelector(".comty-inspector-swatch").style.backgroundColor = getComputedStyle(target)[button.dataset.property === "background" ? "backgroundColor" : "color"];
5010
+ if (selected || draftPreview) panel.dataset.selected = "";
5011
+ const priority = ["background", "color", "font-family", "font-size", "font-weight", "margin", "line-height", "letter-spacing"];
5012
+ const orderedRows = [...rows.filter(([key]) => priority.includes(key)).sort((a, b) => priority.indexOf(a[0]) - priority.indexOf(b[0])), ...rows.filter(([key]) => !priority.includes(key))];
5013
+ const labels = { "font-family": "Font", "font-size": "Size", "font-weight": "Weight", "line-height": "Line height", "letter-spacing": "Spacing", "background": "Background", "color": "Color", "margin": "Margin" };
5014
+ const cssProperty = key => ({ radius: "border-radius", background: "background-color" })[key] || key;
5015
+ const copyValue = (value, tokens) => tokens?.length ? `var(${tokens[0]})` : value;
5016
+ const allProperties = `${selector} {\n${rows.map(([key, value, tokens]) => ` ${cssProperty(key)}: ${copyValue(value, tokens)};`).join("\n")}\n}`;
5017
+ const copyButton = (value, label) => `<button type="button" class="comty-inspector-copy" data-copy="${escapeInspectorText(value)}" aria-label="${escapeInspectorText(label)}" title="${escapeInspectorText(label)}"><i class="comty-property-copy-icon" aria-hidden="true">${copyIcon}${checkIcon}</i></button>`;
5018
+ panel.innerHTML = `<div class="comty-inspector-head"><span class="comty-inspector-selector" dir="rtl" title="${escapeInspectorText(selector)}"><bdi dir="ltr">${escapeInspectorText(selector)}</bdi></span>${copyButton(allProperties, "Copy all properties")}</div><div class="comty-inspector-details">${orderedRows.map(([key, value, tokens]) => {
5019
+ const label = labels[key] || key.replaceAll("-", " ").replace(/^./, c => c.toUpperCase());
5020
+ const isColor = key === "background" || key === "color";
5021
+ const picker = isColor ? "color" : key.startsWith("font-") || ["line-height", "letter-spacing"].includes(key) ? "font" : "";
5022
+ const displayValue = isColor ? colorDetails(value)?.hex || value : value;
5023
+ const valueMarkup = `${isColor ? '<span class="comty-inspector-swatch" aria-hidden="true"></span>' : ""}<span class="comty-inspector-text">${tokens?.length ? `<span class="comty-inspector-tokens">${tokens.map(escapeInspectorText).join(", ")}</span><span class="comty-inspector-secondary">${escapeInspectorText(displayValue)}</span>` : escapeInspectorText(displayValue)}</span>`;
5024
+ const valueElement = picker && selected
5025
+ ? `<button type="button" class="comty-inspector-value comty-inspector-picker" data-picker="${picker}" data-property="${key}" aria-label="Open ${isColor ? key + " colour" : "typography"} details">${valueMarkup}</button>`
5026
+ : `<span class="comty-inspector-value" data-property="${key}">${valueMarkup}</span>`;
5027
+ return `<div class="comty-inspector-row"><span class="comty-inspector-label">${label}</span>${valueElement}${copyButton(tokens?.length ? tokens[0].replace(/^--/, "") : value, `Copy ${label}`)}</div>`;
5028
+ }).join("")}</div><span class="comty-property-feedback" role="status"></span>`;
5029
+ for (const swatch of panel.querySelectorAll('.comty-inspector-swatch')) {
5030
+ const key = swatch.parentElement.dataset.property;
5031
+ swatch.style.backgroundColor = rows.find(([name]) => name === key)?.[1];
5000
5032
  }
5001
5033
  panel.onclick = async (event) => {
5002
5034
  const button = event.target.closest("button");
5003
5035
  if (!button) return;
5004
- if (button.classList.contains("comty-inspector-expand")) {
5005
- inspectExpanded = !inspectExpanded;
5006
- renderInspector();
5007
- } else if (button.dataset.picker) {
5008
- openPropertyPanel(button.dataset.picker, target, button.dataset.property, button);
5009
- } else {
5010
- try {
5011
- await navigator.clipboard.writeText(button.dataset.copy);
5012
- button.title = "Copied";
5013
- } catch {
5014
- button.title = "Clipboard unavailable — select the value to copy";
5015
- }
5016
- }
5036
+ if (button.dataset.picker) openPropertyPanel(button.dataset.picker, target, button.dataset.property, button);
5037
+ else await copyPropertyValue(button, panel.querySelector(".comty-property-feedback"));
5017
5038
  };
5018
5039
  inspectionLayer.appendChild(panel);
5019
5040
  const bounds = panel.getBoundingClientRect();
@@ -5031,8 +5052,24 @@ function initComty(options = {}) {
5031
5052
  }
5032
5053
  return;
5033
5054
  }
5034
- panel.style.left = `${Math.max(8, Math.min(rect.left + rect.width / 2 - bounds.width / 2, innerWidth - bounds.width - 8))}px`;
5035
- panel.style.top = `${Math.max(8, Math.min(rect.bottom + 4 + bounds.height <= innerHeight ? rect.bottom + 4 : rect.top - bounds.height - 4, innerHeight - bounds.height - 8))}px`;
5055
+ const inset = 8, gap = 12;
5056
+ const clampX = x => Math.max(inset, Math.min(x, innerWidth - bounds.width - inset));
5057
+ const clampY = y => Math.max(inset, Math.min(y, innerHeight - bounds.height - inset));
5058
+ const x = clampX(rect.left), y = clampY(rect.top);
5059
+ const candidates = [
5060
+ { left: x, top: rect.bottom + gap },
5061
+ { left: rect.right + gap, top: y },
5062
+ { left: rect.left - bounds.width - gap, top: y },
5063
+ { left: x, top: rect.top - bounds.height - gap },
5064
+ ];
5065
+ let position = candidates.find(({ left, top }) => left >= inset && top >= inset && left + bounds.width <= innerWidth - inset && top + bounds.height <= innerHeight - inset);
5066
+ if (!position) {
5067
+ // A viewport-sized target leaves no outside space; minimize the covered area.
5068
+ const overlap = ({ left, top }) => Math.max(0, Math.min(left + bounds.width, rect.right) - Math.max(left, rect.left)) * Math.max(0, Math.min(top + bounds.height, rect.bottom) - Math.max(top, rect.top));
5069
+ position = candidates.map(({ left, top }) => ({ left: clampX(left), top: clampY(top) })).sort((a, b) => overlap(a) - overlap(b))[0];
5070
+ }
5071
+ panel.style.left = `${position.left}px`;
5072
+ panel.style.top = `${position.top}px`;
5036
5073
  }
5037
5074
 
5038
5075
  function endSession() {
@@ -5047,13 +5084,6 @@ function initComty(options = {}) {
5047
5084
  setSession("edit", { renderComments: true });
5048
5085
  }
5049
5086
 
5050
- function startAgentSession() {
5051
- if (!isLocalSite) return;
5052
- if (session.agent) return;
5053
- closeBubbles();
5054
- setSession("agent", { renderComments: true });
5055
- }
5056
-
5057
5087
  function toggleSidepanel() {
5058
5088
  if (sidepanelOpen) {
5059
5089
  setSidepanelOpen(false);
@@ -5077,7 +5107,7 @@ function initComty(options = {}) {
5077
5107
  showAuthPrompt();
5078
5108
  return;
5079
5109
  }
5080
- if (session.agent || session.inspect || !isSessionActive()) {
5110
+ if (session.inspect || !isSessionActive()) {
5081
5111
  startEditSession();
5082
5112
  return;
5083
5113
  }
@@ -5428,15 +5458,6 @@ function initComty(options = {}) {
5428
5458
  return element;
5429
5459
  }
5430
5460
 
5431
- async function waitForCommentTargetReady(comment) {
5432
- const el = resolveCommentElement(comment);
5433
- if (!el) return false;
5434
-
5435
- await waitForCustomElements(el);
5436
- await waitForPaint();
5437
- return true;
5438
- }
5439
-
5440
5461
  function getDeviceContext() {
5441
5462
  const width = window.innerWidth;
5442
5463
  const height = window.innerHeight;
@@ -5511,125 +5532,44 @@ function initComty(options = {}) {
5511
5532
  if (!session.edit || hadDraft) hideInspector();
5512
5533
  document
5513
5534
  .querySelectorAll(
5514
- ".comty-bubble, .comty-create-bubble, .comty-temp-pin, .comty-popover",
5535
+ ".comty-bubble:not([data-comty-capture-keep]), .comty-create-bubble, .comty-temp-pin, .comty-popover",
5515
5536
  )
5516
5537
  .forEach((x) => x.remove());
5517
5538
  }
5518
5539
 
5519
- const { waitForPaint, waitForCustomElements, captureViewportScreenshot } = createScreenshotCapture();
5520
-
5521
- async function buildCommentScreenshotFormData(payload) {
5522
- const formData = new FormData();
5523
-
5524
- Object.entries(payload).forEach(([key, value]) => {
5525
- if (value === undefined || value === null) return;
5526
- formData.append(key, String(value));
5527
- });
5528
-
5529
- try {
5530
- await waitForCommentTargetReady(payload);
5531
- const screenshot = await captureViewportScreenshot();
5532
- if (screenshot) {
5533
- const extension =
5534
- screenshot.blob.type === "image/svg+xml"
5535
- ? "svg"
5536
- : screenshot.blob.type === "image/jpeg"
5537
- ? "jpg"
5538
- : screenshot.blob.type === "image/png"
5539
- ? "png"
5540
- : "webp";
5541
- formData.append(
5542
- "screenshot",
5543
- screenshot.blob,
5544
- `comment.${extension}`,
5545
- );
5546
- formData.append("screenshotWidth", String(screenshot.width));
5547
- formData.append("screenshotHeight", String(screenshot.height));
5548
- }
5549
- } catch (error) {
5550
- console.error("Comty: screenshot capture failed", error);
5551
- }
5552
-
5553
- return formData;
5554
- }
5540
+ const { captureViewportScreenshot } = createScreenshotCapture({ apiUrl, projectKey, getHeaders: getSdkHeaders });
5555
5541
 
5556
- async function uploadCommentScreenshot(
5557
- commentId,
5558
- payload,
5559
- captureNodes = [],
5560
- ) {
5542
+ async function uploadCommentScreenshot(comment, context, point) {
5561
5543
  try {
5562
- const formData = await buildCommentScreenshotFormData({
5563
- projectKey,
5564
- host: window.location.host,
5565
- ...payload,
5566
- });
5567
-
5568
- captureNodes.forEach((node) => node.remove());
5569
-
5570
- if (!formData.has("screenshot")) return;
5571
-
5544
+ const screenshot = await captureViewportScreenshot({ context, annotation: () => mountCaptureAnnotation(comment, point) });
5545
+ if (!screenshot) return;
5546
+ const formData = new FormData();
5547
+ formData.append("projectKey", projectKey);
5548
+ formData.append("screenshot", screenshot.blob, screenshot.blob.type === "image/jpeg" ? "comment.jpg" : "comment.png");
5549
+ formData.append("screenshotWidth", String(screenshot.width));
5550
+ formData.append("screenshotHeight", String(screenshot.height));
5572
5551
  const response = await fetch(
5573
- `${apiUrl}/api/comments/${encodeURIComponent(commentId)}/screenshot`,
5574
- {
5575
- method: "POST",
5576
- headers: getSdkHeaders(),
5577
- body: formData,
5578
- },
5552
+ `${apiUrl}/api/comments/${encodeURIComponent(comment.id)}/screenshot`,
5553
+ { method: "POST", headers: getSdkHeaders(), body: formData },
5579
5554
  );
5580
-
5581
- if (!response.ok) {
5582
- console.error(
5583
- "Comty: screenshot upload failed",
5584
- await response.text(),
5585
- );
5586
- return;
5587
- }
5588
-
5589
- await loadComments({ force: true });
5555
+ if (!response.ok) throw new Error(`Screenshot upload failed (${response.status})`);
5556
+ // Updating metadata must not redraw the page or interrupt a new draft.
5557
+ const { screenshotUrl } = await response.json();
5558
+ const cached = cachedComments.find(item => String(item.id) === String(comment.id));
5559
+ if (cached) Object.assign(cached, {
5560
+ screenshot_url: screenshotUrl,
5561
+ screenshot_width: screenshot.width,
5562
+ screenshot_height: screenshot.height,
5563
+ });
5590
5564
  } catch (error) {
5591
- console.error("Comty: screenshot upload failed", error);
5592
- captureNodes.forEach((node) => node.remove());
5593
- }
5594
- }
5595
-
5596
- function createCaptureAnnotation(pin, bubble, content) {
5597
- const capturePin = pin.cloneNode(false);
5598
- capturePin.dataset.comtyCaptureKeep = "true";
5599
- capturePin.classList.add("comty-capture-annotation");
5600
- capturePin.classList.remove("comty-temp-pin");
5601
- capturePin.style.display = "grid";
5602
- capturePin.style.pointerEvents = "none";
5603
- capturePin.style.animation = "none";
5604
- capturePin.style.opacity = "1";
5605
- capturePin.style.transform = "translate(-50%, -50%)";
5606
- setPinIcon(capturePin, currentUser || { name: "You" });
5607
-
5608
- const captureBubble = document.createElement("div");
5609
- captureBubble.className =
5610
- "comty-bubble comty-hover-bubble comty-capture-annotation";
5611
- captureBubble.dataset.comtyCaptureKeep = "true";
5612
- captureBubble.style.left = bubble.style.left;
5613
- captureBubble.style.top = bubble.style.top;
5614
- captureBubble.style.display = "block";
5615
- captureBubble.style.pointerEvents = "none";
5616
- captureBubble.style.animation = "none";
5617
- captureBubble.style.opacity = "1";
5618
- captureBubble.style.transform = "none";
5619
-
5620
- const contentEl = document.createElement("div");
5621
- contentEl.className = "comty-bubble-content";
5622
- contentEl.innerText = content;
5623
- captureBubble.appendChild(contentEl);
5624
-
5625
- document.body.appendChild(capturePin);
5626
- document.body.appendChild(captureBubble);
5627
- return [capturePin, captureBubble];
5565
+ console.warn("Comty: comment saved, screenshot unavailable", error);
5566
+ }
5628
5567
  }
5629
5568
 
5630
5569
  function addLocalComment(commentId, payload) {
5631
5570
  const comment = {
5632
5571
  id: commentId,
5572
+ pending: commentId.startsWith("pending-"),
5633
5573
  project_id: projectId,
5634
5574
  domain: payload.host || window.location.host,
5635
5575
  page_path: payload.pagePath || window.location.pathname,
@@ -5637,6 +5577,7 @@ function initComty(options = {}) {
5637
5577
  rel_x: payload.relX,
5638
5578
  rel_y: payload.relY,
5639
5579
  content: payload.content || "",
5580
+ link_url: payload.linkUrl || null,
5640
5581
  created_at: new Date().toISOString(),
5641
5582
  dom_element: payload.domElement || null,
5642
5583
  viewport: payload.viewport || null,
@@ -5648,7 +5589,9 @@ function initComty(options = {}) {
5648
5589
  screenshot_width: null,
5649
5590
  screenshot_height: null,
5650
5591
  anchor_context: payload.anchorContext || null,
5651
- author_user_id: currentUser?.id || null,
5592
+ author_user_id: currentUser?.isGuest ? null : currentUser?.id || null,
5593
+ guest_author_id: currentUser?.isGuest ? currentUser.id : null,
5594
+ guest_author_name: currentUser?.isGuest ? currentUser.name : null,
5652
5595
  author_name: currentUser?.name || null,
5653
5596
  author_avatar_url: currentUser?.avatarUrl || null,
5654
5597
  replies: [],
@@ -5663,6 +5606,7 @@ function initComty(options = {}) {
5663
5606
  ];
5664
5607
  renderSidepanelComments();
5665
5608
  void renderComments(cachedComments, { retry: false });
5609
+ return comment;
5666
5610
  }
5667
5611
 
5668
5612
  function getCommentAuthorName(comment) {
@@ -5672,8 +5616,7 @@ function initComty(options = {}) {
5672
5616
  function isOwnCommentEntry(entry) {
5673
5617
  return Boolean(
5674
5618
  currentUser?.id &&
5675
- entry?.author_user_id &&
5676
- entry.author_user_id === currentUser.id,
5619
+ (currentUser.isGuest ? entry?.guest_author_id : entry?.author_user_id) === currentUser.id,
5677
5620
  );
5678
5621
  }
5679
5622
 
@@ -5749,7 +5692,7 @@ function initComty(options = {}) {
5749
5692
 
5750
5693
  function createThreadItem(
5751
5694
  entry,
5752
- { showMore = isOwnCommentEntry(entry) } = {},
5695
+ { showMore = isOwnCommentEntry(entry), showLink = true } = {},
5753
5696
  ) {
5754
5697
  const item = document.createElement("div");
5755
5698
  item.className = "comty-thread-item";
@@ -5784,15 +5727,51 @@ function initComty(options = {}) {
5784
5727
 
5785
5728
  const content = document.createElement("div");
5786
5729
  content.className = "comty-bubble-content";
5787
- content.innerText = entry?.content || "";
5730
+ renderCommentContent(content, entry?.content || "");
5788
5731
 
5789
5732
  body.appendChild(author);
5790
5733
  body.appendChild(content);
5734
+ if (showLink && entry?.link_url) body.appendChild(createCommentLink(entry.link_url));
5791
5735
  item.appendChild(body);
5792
5736
 
5793
5737
  return { item, body, content, moreButton };
5794
5738
  }
5795
5739
 
5740
+ function renderCommentContent(element, value) {
5741
+ const content = String(value ?? "");
5742
+ const nodes = [];
5743
+ let previousEnd = 0;
5744
+ for (const match of content.matchAll(/https?:\/\/[^\s<>"']+/gi)) {
5745
+ const url = match[0].replace(/[.,!?;:]+$/, "");
5746
+ if (!url) continue;
5747
+ let parsed;
5748
+ try { parsed = new URL(url); } catch { continue; }
5749
+ if (!["http:", "https:"].includes(parsed.protocol) || !parsed.hostname) continue;
5750
+ nodes.push(document.createTextNode(content.slice(previousEnd, match.index)));
5751
+ const link = document.createElement("a");
5752
+ link.href = url;
5753
+ link.target = "_blank";
5754
+ link.rel = "noopener noreferrer";
5755
+ link.textContent = url;
5756
+ nodes.push(link);
5757
+ previousEnd = match.index + url.length;
5758
+ }
5759
+ nodes.push(document.createTextNode(content.slice(previousEnd)));
5760
+ element.replaceChildren(...nodes);
5761
+ }
5762
+
5763
+ function createCommentLink(url) {
5764
+ const link = document.createElement("a");
5765
+ link.className = "comty-comment-link";
5766
+ link.href = url;
5767
+ link.target = "_blank";
5768
+ link.rel = "noopener noreferrer";
5769
+ link.title = url;
5770
+ link.setAttribute("aria-label", `Open attached link: ${url}`);
5771
+ link.textContent = url;
5772
+ return link;
5773
+ }
5774
+
5796
5775
  function setPinIcon(pin, comment = null) {
5797
5776
  if (comment) {
5798
5777
  pin.classList.add("comty-pin-with-avatar");
@@ -5809,6 +5788,54 @@ function initComty(options = {}) {
5809
5788
  `;
5810
5789
  }
5811
5790
 
5791
+ function createCommentPin(comment, x, y) {
5792
+ const pin = document.createElement("button");
5793
+ pin.className = "comty-pin";
5794
+ pin.type = "button";
5795
+ pin.style.left = `${x}px`;
5796
+ pin.style.top = `${y}px`;
5797
+ setPinIcon(pin, comment);
5798
+ const statusDot = document.createElement("span");
5799
+ statusDot.className = "comty-pin-status-dot";
5800
+ applyStatusDot(statusDot, comment.status);
5801
+ pin.appendChild(statusDot);
5802
+ return pin;
5803
+ }
5804
+
5805
+ function createCommentPreview(comment) {
5806
+ const bubble = document.createElement("div");
5807
+ bubble.className = "comty-bubble comty-hover-bubble";
5808
+ const thread = document.createElement("div");
5809
+ thread.className = "comty-thread";
5810
+ thread.appendChild(createThreadItem(comment, { showMore: false, showLink: false }).item);
5811
+ bubble.appendChild(thread);
5812
+ return bubble;
5813
+ }
5814
+
5815
+ function mountCaptureAnnotation(comment, { x, y }) {
5816
+ const host = document.createElement("div");
5817
+ host.dataset.comtyCaptureExclude = "";
5818
+ host.setAttribute("aria-hidden", "true");
5819
+ host.inert = true;
5820
+ // Ancestor opacity hides measurement without changing the components'
5821
+ // computed opacity. Nothing flashes or intercepts input during cloning.
5822
+ Object.assign(host.style, { position: "fixed", inset: "0", opacity: "0", pointerEvents: "none" });
5823
+ const pin = createCommentPin(comment, x, y);
5824
+ const bubble = createCommentPreview(comment);
5825
+ for (const element of [pin, bubble]) {
5826
+ element.dataset.comtyCaptureKeep = "";
5827
+ element.style.setProperty("animation", "none", "important");
5828
+ element.style.setProperty("transition", "none", "important");
5829
+ element.style.setProperty("pointer-events", "none", "important");
5830
+ }
5831
+ pin.style.display = "grid";
5832
+ bubble.style.display = "block";
5833
+ host.append(pin, bubble);
5834
+ root.appendChild(host);
5835
+ positionCommentBubble(bubble, x, y);
5836
+ return { elements: [pin, bubble], dispose: () => host.remove() };
5837
+ }
5838
+
5812
5839
  function createCommentAuthor(comment) {
5813
5840
  const author = document.createElement("div");
5814
5841
  author.className = "comty-comment-author";
@@ -5852,74 +5879,126 @@ function initComty(options = {}) {
5852
5879
 
5853
5880
  const textarea = document.createElement("textarea");
5854
5881
  textarea.className = "comty-create-textarea";
5855
- textarea.placeholder = payload.kind === "agent" ? "Describe the change for your agent" : "Add a comment";
5882
+ textarea.placeholder = "Add a comment";
5856
5883
  textarea.setAttribute("aria-label", textarea.placeholder);
5857
5884
  textarea.rows = 1;
5858
5885
 
5859
5886
  const actions = document.createElement("div");
5860
5887
  actions.className = "comty-create-actions";
5861
5888
 
5889
+ const linkBtn = document.createElement("button");
5890
+ linkBtn.type = "button";
5891
+ linkBtn.className = "comty-create-link-button";
5892
+ linkBtn.setAttribute("aria-label", "Add link");
5893
+ linkBtn.innerHTML = `
5894
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
5895
+ <path d="M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
5896
+ <path d="M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
5897
+ </svg>
5898
+ `;
5899
+
5900
+ const linkEditor = document.createElement("div");
5901
+ linkEditor.className = "comty-create-link-editor";
5902
+ linkEditor.inert = true;
5903
+ linkEditor.setAttribute("aria-hidden", "true");
5904
+ const linkInput = document.createElement("input");
5905
+ linkInput.type = "url";
5906
+ linkInput.placeholder = "Paste a link";
5907
+ linkInput.setAttribute("aria-label", "Link URL");
5908
+ const linkInsert = document.createElement("button");
5909
+ linkInsert.type = "button";
5910
+ linkInsert.textContent = "Add";
5911
+ const linkCancel = document.createElement("button");
5912
+ linkCancel.type = "button";
5913
+ linkCancel.setAttribute("aria-label", "Cancel link");
5914
+ linkCancel.textContent = "×";
5915
+ const linkField = document.createElement("div");
5916
+ linkField.className = "comty-create-link-field";
5917
+ linkField.append(linkInput, linkCancel);
5918
+ linkEditor.append(linkField, linkInsert);
5919
+
5920
+ const attachment = document.createElement("div");
5921
+ attachment.className = "comty-create-attachment";
5922
+ const attachmentLink = document.createElement("a");
5923
+ attachmentLink.className = "comty-comment-link";
5924
+ attachmentLink.target = "_blank";
5925
+ attachmentLink.rel = "noopener noreferrer";
5926
+ const removeAttachment = document.createElement("button");
5927
+ removeAttachment.type = "button";
5928
+ removeAttachment.setAttribute("aria-label", "Remove attached link");
5929
+ removeAttachment.textContent = "×";
5930
+ const attachmentChip = document.createElement("div");
5931
+ attachmentChip.className = "comty-create-attachment-chip";
5932
+ attachmentChip.append(attachmentLink, removeAttachment);
5933
+ attachment.appendChild(attachmentChip);
5934
+ let attachedLink = "";
5935
+
5862
5936
  const submitBtn = document.createElement("button");
5937
+ submitBtn.type = "button";
5863
5938
  submitBtn.className = "comty-btn-submit";
5864
- submitBtn.setAttribute("aria-label", payload.kind === "agent" ? "Send agent task" : "Post comment");
5939
+ submitBtn.setAttribute("aria-label", "Post comment");
5940
+ submitBtn.disabled = true;
5865
5941
  submitBtn.innerHTML = `
5866
5942
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
5867
5943
  <path d="M12 19V5M5 12l7-7 7 7"/>
5868
5944
  </svg>
5869
5945
  `;
5870
5946
 
5947
+ const editorDraft = draftAnchor;
5871
5948
  const submit = async () => {
5872
5949
  const content = textarea.value.trim();
5873
- if (!content) return;
5950
+ if (!content || textarea.disabled) return;
5874
5951
 
5875
5952
  submitBtn.disabled = true;
5876
5953
  textarea.disabled = true;
5954
+ linkBtn.disabled = true;
5877
5955
 
5878
- if (payload.kind === "agent" && !agent) {
5879
- showAgentBridgeError(); submitBtn.disabled = false; textarea.disabled = false; return;
5880
- }
5881
5956
  payload.content = content;
5957
+ payload.linkUrl = attachedLink || null;
5882
5958
  textarea.textContent = content;
5883
5959
 
5960
+ const context = captureContext();
5961
+ const point = getPinPoint(pin);
5962
+ const pendingId = `pending-${crypto.randomUUID()}`;
5963
+ // Paint the comment immediately. The network save never waits for capture work.
5964
+ bubble.remove();
5965
+ pin.remove();
5966
+ if (draftAnchor === editorDraft) draftAnchor = null;
5967
+ hideInspector();
5968
+ addLocalComment(pendingId, payload);
5884
5969
  let post;
5885
5970
  try {
5886
- post = await fetch(payload.kind === "agent" ? `${agentBridgeUrl}/tasks` : `${apiUrl}/api/comments`, {
5971
+ post = await fetch(`${apiUrl}/api/comments`, {
5887
5972
  method: "POST",
5888
- headers: payload.kind === "agent" ? agentHeaders() : getSdkHeaders({ "Content-Type": "application/json" }),
5889
- body: JSON.stringify(payload.kind === "agent" ? { ...payload, projectKey } : payload),
5973
+ headers: getSdkHeaders({ "Content-Type": "application/json" }),
5974
+ body: JSON.stringify(payload),
5890
5975
  });
5976
+ if (!post.ok) throw new Error(`Comment save failed (${post.status})`);
5977
+ const data = await post.json();
5978
+ if (!data?.id) throw new Error("Comment save returned no ID");
5979
+ cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
5980
+ if (context.url === location.href) {
5981
+ const comment = addLocalComment(String(data.id), payload);
5982
+ // Start only after the save and immediate UI update; the capture yields to input.
5983
+ void uploadCommentScreenshot(comment, context, point);
5984
+ } else void loadComments({ force: true });
5891
5985
  } catch (error) {
5892
5986
  console.error("Comty: save failed", error);
5893
- if (payload.kind === "agent") showAgentBridgeError();
5894
- submitBtn.disabled = false;
5895
- textarea.disabled = false;
5896
- return;
5897
- }
5898
-
5899
- if (!post.ok) {
5900
- console.error("Comty: save failed", await post.text());
5901
- if (payload.kind === "agent") showAgentBridgeError();
5987
+ cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
5988
+ renderSidepanelComments();
5989
+ void renderComments(cachedComments, { retry: false });
5990
+ // Keep the original editor and its text available for retry, including attachments.
5991
+ if (!draftAnchor) draftAnchor = editorDraft;
5992
+ root.append(pin, bubble);
5902
5993
  submitBtn.disabled = false;
5903
5994
  textarea.disabled = false;
5904
- return;
5905
- }
5906
-
5907
- const data = await post.json();
5908
- const commentId = payload.kind === "agent" ? data?.task?.id : data?.id;
5909
- const captureNodes = commentId && payload.kind !== "agent"
5910
- ? createCaptureAnnotation(pin, bubble, content)
5911
- : [];
5912
- bubble.remove();
5913
- pin.remove();
5914
- draftAnchor = null;
5915
- hideInspector();
5916
- if (commentId) {
5917
- if (payload.kind === "agent") showAgentTaskToast(String(commentId));
5918
- else addLocalComment(String(commentId), payload);
5919
- void loadComments({ force: true });
5920
- if (payload.kind !== "agent") void uploadCommentScreenshot(String(commentId), payload, captureNodes);
5921
- } else {
5922
- void loadComments({ force: true });
5995
+ linkBtn.disabled = false;
5996
+ const errorMessage = document.createElement("p");
5997
+ errorMessage.setAttribute("role", "alert");
5998
+ errorMessage.textContent = "Comment not saved. Your draft is ready to retry.";
5999
+ bubble.querySelector('[role="alert"]')?.remove();
6000
+ bubble.append(errorMessage);
6001
+ if (bubble.isConnected) textarea.focus();
5923
6002
  }
5924
6003
  };
5925
6004
 
@@ -5928,8 +6007,63 @@ function initComty(options = {}) {
5928
6007
  submit();
5929
6008
  };
5930
6009
 
6010
+ const closeLinkEditor = () => {
6011
+ bubble.classList.remove("comty-create-link-open");
6012
+ linkEditor.inert = true;
6013
+ linkEditor.setAttribute("aria-hidden", "true");
6014
+ linkInput.value = "";
6015
+ linkInput.removeAttribute("aria-invalid");
6016
+ linkInput.setCustomValidity("");
6017
+ textarea.focus();
6018
+ };
6019
+ linkBtn.onclick = () => {
6020
+ if (!bubble.classList.contains("comty-create-expanded")) return;
6021
+ bubble.classList.add("comty-create-link-open");
6022
+ linkEditor.inert = false;
6023
+ linkEditor.setAttribute("aria-hidden", "false");
6024
+ linkInput.focus();
6025
+ };
6026
+ linkCancel.onclick = closeLinkEditor;
6027
+ linkInput.oninput = () => {
6028
+ linkInput.removeAttribute("aria-invalid");
6029
+ linkInput.setCustomValidity("");
6030
+ };
6031
+ linkInput.onkeydown = (event) => {
6032
+ if (event.key === "Enter") { event.preventDefault(); linkInsert.click(); }
6033
+ if (event.key === "Escape") { event.preventDefault(); closeLinkEditor(); }
6034
+ };
6035
+ linkInsert.onclick = () => {
6036
+ const value = linkInput.value.trim();
6037
+ const candidate = /^[a-z][a-z0-9+.-]*:/i.test(value) ? value : `https://${value}`;
6038
+ let url;
6039
+ try { url = new URL(candidate); } catch { /* Report below. */ }
6040
+ if (!url || !["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password || /\s/.test(value) || String(url).length > 2048) {
6041
+ linkInput.setAttribute("aria-invalid", "true");
6042
+ linkInput.setCustomValidity("Enter a valid HTTP(S) URL.");
6043
+ linkInput.reportValidity();
6044
+ linkInput.focus();
6045
+ return;
6046
+ }
6047
+ attachedLink = String(url);
6048
+ attachmentLink.href = attachedLink;
6049
+ attachmentLink.title = attachedLink;
6050
+ attachmentLink.textContent = attachedLink;
6051
+ bubble.classList.add("comty-create-has-attachment");
6052
+ closeLinkEditor();
6053
+ const point = getPinPoint(pin);
6054
+ positionCommentBubble(bubble, point.x, point.y);
6055
+ renderInspector();
6056
+ };
6057
+
6058
+ removeAttachment.onclick = () => {
6059
+ attachedLink = "";
6060
+ bubble.classList.remove("comty-create-has-attachment");
6061
+ textarea.focus();
6062
+ textarea.oninput();
6063
+ };
6064
+
5931
6065
  textarea.onkeydown = (e) => {
5932
- if (e.key === "Enter" && !e.shiftKey) {
6066
+ if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
5933
6067
  e.preventDefault();
5934
6068
  submit();
5935
6069
  }
@@ -5940,31 +6074,20 @@ function initComty(options = {}) {
5940
6074
  };
5941
6075
 
5942
6076
  textarea.oninput = () => {
5943
- textarea.style.height = "24px";
5944
- const scrollHeight = textarea.scrollHeight;
5945
- textarea.style.height = scrollHeight + "px";
5946
-
5947
- if (scrollHeight > 24) {
5948
- bubble.style.borderRadius = "16px";
5949
- bubble.style.alignItems = "flex-end";
5950
- } else {
5951
- bubble.style.borderRadius = "999px";
5952
- bubble.style.alignItems = "center";
5953
- }
5954
-
5955
- if (textarea.value.trim().length > 0) {
5956
- submitBtn.classList.add("comty-btn-active");
5957
- } else {
5958
- submitBtn.classList.remove("comty-btn-active");
5959
- }
6077
+ const expanded = textarea.value.length > 0 || Boolean(attachedLink);
6078
+ bubble.classList.toggle("comty-create-expanded", expanded);
6079
+ if (!expanded && bubble.classList.contains("comty-create-link-open")) closeLinkEditor();
6080
+ submitBtn.disabled = !textarea.value.trim();
6081
+ submitBtn.classList.toggle("comty-btn-active", !submitBtn.disabled);
5960
6082
  const point = getPinPoint(pin);
5961
6083
  positionCommentBubble(bubble, point.x, point.y);
5962
6084
  renderInspector();
5963
6085
  };
5964
6086
 
5965
- actions.appendChild(submitBtn);
6087
+ actions.append(linkBtn, submitBtn, linkEditor);
5966
6088
 
5967
6089
  bubble.appendChild(textarea);
6090
+ bubble.appendChild(attachment);
5968
6091
  bubble.appendChild(actions);
5969
6092
 
5970
6093
  root.appendChild(bubble);
@@ -5974,223 +6097,6 @@ function initComty(options = {}) {
5974
6097
  setTimeout(() => textarea.focus(), 10);
5975
6098
  }
5976
6099
 
5977
- function agentHeaders() { return { "Content-Type": "application/json", "X-Comty-Token": agent?.token || "" }; }
5978
-
5979
- function showAgentBridgeError(message = "Start your app with comty dev to use the local agent. Check the terminal if it is already running.") {
5980
- root.querySelectorAll(".comty-agent-task-toast").forEach((node) => node.remove());
5981
- const toast = document.createElement("div");
5982
- toast.className = "comty-auth-toast comty-agent-task-toast";
5983
- toast.setAttribute("role", "alert");
5984
- const issue = describeAgentError(message);
5985
- toast.textContent = issue.title + ". " + issue.description;
5986
- root.appendChild(toast);
5987
- setTimeout(() => toast.remove(), 5000);
5988
- }
5989
-
5990
- function showAgentTaskToast() {
5991
- agentPanelOpen = true;
5992
- agentPanelSignature = "";
5993
- renderAgentPanel();
5994
- }
5995
-
5996
- function describeAgentError(error) {
5997
- const raw = typeof error === 'string' ? error : JSON.stringify(error, null, 2) || 'Unknown error';
5998
- let value = error;
5999
- for (let depth = 0; depth < 8; depth++) {
6000
- if (typeof value === 'string') {
6001
- try { value = JSON.parse(value); continue; } catch {}
6002
- const start = value.indexOf('{'), end = value.lastIndexOf('}');
6003
- if (start >= 0 && end > start) { try { value = JSON.parse(value.slice(start, end + 1)); continue; } catch {} }
6004
- break;
6005
- }
6006
- if (!value || typeof value !== 'object') break;
6007
- value = value.error || value.message || value.cause || value.stderr || value;
6008
- }
6009
- const message = typeof value === 'string' ? value : '';
6010
- const source = raw + ' ' + message;
6011
- const agentName = /codex/i.test(source) ? 'Codex' : /claude/i.test(source) ? 'Claude Code' : /cursor/i.test(source) ? 'Cursor' : 'your coding agent';
6012
- let title, description;
6013
- if (/requires a newer version|upgrade.*latest|outdated|unsupported.*version/i.test(source)) {
6014
- title = 'Agent update required'; description = `Update ${agentName}, restart your local development session, then retry this task.`;
6015
- } else if (/insufficient_quota|quota exceeded|credit balance|billing|usage limit/i.test(source)) {
6016
- title = 'Usage limit reached'; description = 'Check your agent account’s usage allowance or billing, then retry when capacity is available.';
6017
- } else if (/rate.?limit|too many requests|\b429\b/i.test(source)) {
6018
- title = 'Too many requests'; description = 'The agent is temporarily rate limited. Wait a moment before retrying.';
6019
- } else if (/unauthorized|unauthenticated|authentication|not logged|sign.?in|log.?in|invalid.api.key|\b401\b/i.test(source)) {
6020
- title = 'Sign in to your agent'; description = `Open ${agentName} and sign in again, then retry this task.`;
6021
- } else if (/model.*(?:not found|does not exist|unavailable|not supported)|unsupported model|model_not_found/i.test(source)) {
6022
- title = 'Model unavailable'; description = 'Choose a model available to your agent account in the local Comty configuration, then restart and retry.';
6023
- } else if (/disconnected|ECONNREFUSED|ENOTFOUND|network|failed to fetch|development server stopped/i.test(source)) {
6024
- title = 'Connection lost'; description = 'Check that your local development server and coding agent are running. Reconnect before retrying; your task history is kept.';
6025
- } else if (/timed? ?out|timeout|exceeded.*minutes/i.test(source)) {
6026
- title = 'Task timed out'; description = 'Review any changes already made, then retry with a smaller, more specific request.';
6027
- } else if (/permission|denied|forbidden|\b403\b/i.test(source)) {
6028
- title = 'Permission needed'; description = 'Check the agent’s access to this project and any pending approval before retrying.';
6029
- } else if (/ENOENT|command not found|not installed|executable.*not found/i.test(source)) {
6030
- title = 'Agent not found'; description = `Install or configure ${agentName} in your local development environment, then restart and retry.`;
6031
- } else if (/completion was not verified/i.test(source)) {
6032
- title = 'Result needs checking'; description = 'The agent could not confirm completion. Review the changes before retrying.';
6033
- } else {
6034
- title = 'Task could not finish';
6035
- description = message && !/[{}]|\[object Object\]/.test(message) ? message.slice(0, 300) : 'The agent reported an unexpected error. Review the technical details and any changes before retrying.';
6036
- }
6037
- return { title, description, raw, message };
6038
- }
6039
-
6040
- function renderAgentPanel() {
6041
- if (!agent) return;
6042
- let panel = root.querySelector(".comty-agent-panel");
6043
- if (!panel) {
6044
- panel = document.createElement("section");
6045
- panel.className = "comty-agent-panel";
6046
- panel.setAttribute("aria-label", "Local agent tasks");
6047
- panel.hidden = true;
6048
- // Keep the dock above inspection overlays and below the toolbar.
6049
- root.insertBefore(panel, toolbar);
6050
- }
6051
- const open = session.agent && agentPanelOpen;
6052
- if (open) {
6053
- if (panel.hidden) {
6054
- const rect = toolbar.getBoundingClientRect();
6055
- panel.dataset.side = rect.left + rect.width / 2 < innerWidth / 2 ? "left" : "right";
6056
- document.documentElement.dataset.comtyAgentSide = panel.dataset.side;
6057
- panel.dataset.closed = "";
6058
- panel.hidden = false;
6059
- panel.getBoundingClientRect();
6060
- }
6061
- if (panel.hasAttribute("data-closed")) { agentPanelTransition++; delete panel.dataset.closed; }
6062
- } else if (!panel.hidden && !panel.hasAttribute("data-closed")) {
6063
- const transition = ++agentPanelTransition;
6064
- panel.dataset.closed = "";
6065
- const finish = () => { if (transition === agentPanelTransition && panel.hasAttribute("data-closed")) panel.hidden = true; };
6066
- if (reducedMotion.matches) finish();
6067
- else setTimeout(finish, parseFloat(getComputedStyle(root).getPropertyValue("--comty-motion-duration")));
6068
- }
6069
- panel.inert = !open;
6070
- panel.setAttribute("aria-hidden", String(!open));
6071
- document.documentElement.classList.toggle("comty-agent-docked", open);
6072
- agentHistoryBtn?.setAttribute("aria-expanded", String(open));
6073
- agentHistoryBtn?.setAttribute("aria-pressed", String(open));
6074
- const signature = JSON.stringify([localTasks, localApprovals, agentConnectionError, agentActionError, agentPanelOpen]);
6075
- if (signature === agentPanelSignature) return;
6076
- agentPanelSignature = signature;
6077
- root.querySelector('.comty-agent-error-popover')?.remove();
6078
- const expanded = new Set([...panel.querySelectorAll('details[data-detail-key][open]')].map(node => node.dataset.detailKey));
6079
- const focusKey = panel.contains(document.activeElement) ? document.activeElement.dataset.focusKey : null;
6080
- const scrollTop = panel.querySelector('.comty-agent-panel-content')?.scrollTop || 0;
6081
- const node = (tag, className, text) => {
6082
- const element = document.createElement(tag);
6083
- if (className) element.className = className;
6084
- if (text != null) element.textContent = text;
6085
- return element;
6086
- };
6087
- const codeIcon = '<path d="m8 6-6 6 6 6m8-12 6 6-6 6m-3-15-2 18"/>';
6088
- const brand = () => { const element = node('span', 'comty-agent-brand'); element.innerHTML = icon(codeIcon); element.setAttribute('aria-hidden', 'true'); return element; };
6089
- panel.replaceChildren();
6090
- const header = node('header', 'comty-agent-panel-header');
6091
- const heading = node('div', 'comty-agent-heading');
6092
- heading.append(node('strong', '', 'Modifications'));
6093
- const toggle = node('button', 'comty-agent-action comty-agent-close'); toggle.type = 'button';
6094
- toggle.innerHTML = icon('<path d="m6 6 12 12M6 18 18 6"/>');
6095
- toggle.setAttribute('aria-label', 'Close local agent sidebar'); toggle.title = 'Close sidebar'; toggle.dataset.focusKey = 'close';
6096
- toggle.onclick = () => { agentPanelOpen = false; renderAgentPanel(); agentHistoryBtn?.focus({ preventScroll: true }); };
6097
- header.append(heading, toggle); panel.appendChild(header);
6098
- const content = node('div', 'comty-agent-panel-content'); panel.appendChild(content);
6099
- const technicalDetails = (parent, raw, key) => {
6100
- const details = node('details', 'comty-agent-details'); details.dataset.detailKey = key; details.open = expanded.has(key);
6101
- const summary = node('summary', '', 'Technical details'); summary.dataset.focusKey = 'details-' + key;
6102
- details.append(summary, node('pre', '', raw)); parent.appendChild(details);
6103
- };
6104
- const errorMessage = (parent, error, key) => {
6105
- const issue = describeAgentError(error);
6106
- const message = node('div', 'comty-agent-error');
6107
- message.append(node('strong', '', issue.title), node('p', '', issue.description));
6108
- parent.appendChild(message); technicalDetails(parent, issue.raw, key);
6109
- };
6110
- if (agentConnectionError || agentActionError) {
6111
- const banner = node('div', 'comty-agent-item comty-agent-banner'); banner.setAttribute('role', 'alert');
6112
- errorMessage(banner, agentConnectionError || agentActionError, 'connection'); content.appendChild(banner);
6113
- }
6114
- const action = (parent, label, path, input = {}, primary = false) => {
6115
- const button = node('button', 'comty-agent-action', label); button.type = 'button';
6116
- const key = path + JSON.stringify(input); button.dataset.focusKey = key;
6117
- button.disabled = agentPendingActions.has(key);
6118
- if (primary) button.dataset.primary = '';
6119
- button.onclick = async () => {
6120
- if (agentPendingActions.has(key)) return;
6121
- agentActionError = '';
6122
- agentPendingActions.add(key); button.disabled = true; button.textContent = 'Please wait…';
6123
- try {
6124
- const response = await fetch(`${agentBridgeUrl}/${path}`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify(input) });
6125
- if (!response.ok) {
6126
- const body = await response.json().catch(() => null);
6127
- throw new Error(typeof body?.error === 'string' ? body.error : JSON.stringify(body?.error || { status: response.status, message: 'Request failed' }));
6128
- }
6129
- await loadComments({ force: true });
6130
- } catch (error) { agentActionError = error.message; }
6131
- finally { agentPendingActions.delete(key); agentPanelSignature = ''; renderAgentPanel(); }
6132
- };
6133
- parent.appendChild(button);
6134
- };
6135
- for (const approval of localApprovals) {
6136
- const section = node('div', 'comty-agent-item');
6137
- section.append(node('h3', '', 'Approval required'));
6138
- const task = localTasks.find(task => task.id === approval.taskId);
6139
- if (task) section.appendChild(node('p', '', task.content));
6140
- section.appendChild(node('p', '', 'Review this operation before allowing the agent to continue.'));
6141
- const operation = approval.details?.command || approval.details?.file_path || approval.details?.path;
6142
- if (typeof operation === 'string' || Array.isArray(operation)) section.appendChild(node('pre', '', Array.isArray(operation) ? operation.join(' ') : operation));
6143
- technicalDetails(section, JSON.stringify(approval.details ?? {}, null, 2), 'approval-' + approval.id);
6144
- const actions = node('div', 'comty-agent-actions');
6145
- action(actions, 'Allow once', `approvals/${approval.id}`, { allow: true }, true);
6146
- action(actions, 'Deny', `approvals/${approval.id}`, { allow: false });
6147
- section.appendChild(actions); content.appendChild(section);
6148
- }
6149
- const list = node('div', 'comty-agent-list'); content.appendChild(list);
6150
- const hideErrorPopover = () => root.querySelector('.comty-agent-error-popover')?.remove();
6151
- content.addEventListener('scroll', hideErrorPopover, { passive: true });
6152
- for (const [index, task] of [...localTasks].reverse().entries()) {
6153
- const failed = Boolean(task.error) || task.status === 'in_review';
6154
- const state = failed ? 'error' : task.status === 'done' ? 'success' : task.status === 'cancel' ? 'cancel' : 'working';
6155
- const row = node('div', 'comty-agent-row'); row.dataset.taskId = task.id;
6156
- const dot = state === 'working' ? createWorkingIndicator() : node('span', 'comty-agent-dot');
6157
- dot.dataset.status = state; dot.setAttribute('aria-hidden', 'true');
6158
- const name = node('span', 'comty-agent-row-name', task.content);
6159
- row.append(dot, name);
6160
- if (state === 'working') row.appendChild(node('span', 'comty-agent-row-state', task.status === 'todo' ? 'Starting' : 'Working'));
6161
- const actions = node('div', 'comty-agent-row-actions');
6162
- if (['in_review', 'cancel'].includes(task.status)) action(actions, 'Retry', `tasks/${task.id}/retry`);
6163
- if (['todo', 'in_progress'].includes(task.status)) action(actions, 'Cancel', `tasks/${task.id}/cancel`);
6164
- if (actions.childElementCount) row.appendChild(actions);
6165
- row.setAttribute('aria-label', `${task.content}, ${state === 'error' ? 'failed' : state === 'success' ? 'completed' : state === 'cancel' ? 'cancelled' : task.status === 'todo' ? 'starting' : 'working'}`);
6166
- if (failed) {
6167
- const tooltipId = `comty-agent-error-${index}`;
6168
- const showErrorPopover = () => {
6169
- hideErrorPopover();
6170
- const issue = describeAgentError(task.error || task.result?.summary || task.progress || 'Completion was not verified');
6171
- const popover = node('div', 'comty-agent-error-popover'); popover.id = tooltipId; popover.setAttribute('role', 'tooltip');
6172
- popover.append(node('strong', '', task.content), node('p', '', issue.message || issue.description));
6173
- root.appendChild(popover);
6174
- const rowRect = row.getBoundingClientRect();
6175
- const popoverRect = popover.getBoundingClientRect();
6176
- const left = panel.dataset.side === 'right' ? rowRect.left - popoverRect.width - 8 : rowRect.right + 8;
6177
- popover.style.left = `${Math.max(8, Math.min(left, innerWidth - popoverRect.width - 8))}px`;
6178
- popover.style.top = `${Math.max(8, Math.min(rowRect.top - 4, innerHeight - popoverRect.height - 8))}px`;
6179
- };
6180
- row.addEventListener('mouseenter', showErrorPopover);
6181
- row.addEventListener('mouseleave', hideErrorPopover);
6182
- }
6183
- list.appendChild(row);
6184
- }
6185
- if (!localTasks.length && !localApprovals.length) {
6186
- const empty = node('div', 'comty-agent-empty');
6187
- empty.append(brand(), node('strong', '', localTasks.length ? 'All caught up' : 'What shall we change?'), node('p', '', 'Point at an element on the page and describe the change you want.'));
6188
- content.appendChild(empty);
6189
- }
6190
- content.scrollTop = scrollTop;
6191
- if (focusKey && open) [...panel.querySelectorAll('[data-focus-key]')].find(element => element.dataset.focusKey === focusKey)?.focus({ preventScroll: true });
6192
- }
6193
-
6194
6100
  function setSidepanelOpen(open) {
6195
6101
  sidepanelOpen = open;
6196
6102
  sidepanel.classList.toggle("comty-sidepanel-open", sidepanelOpen);
@@ -6244,7 +6150,6 @@ function initComty(options = {}) {
6244
6150
  ? sidepanelSearch.value.trim().toLowerCase()
6245
6151
  : "";
6246
6152
  let list = cachedComments.filter((comment) => {
6247
- if (comment.local_agent || comment.kind === "agent") return false;
6248
6153
  if (
6249
6154
  search &&
6250
6155
  !`${comment.content ?? ""} ${getCommentLabel(comment)}`
@@ -6291,6 +6196,7 @@ function initComty(options = {}) {
6291
6196
  const item = document.createElement("button");
6292
6197
  item.type = "button";
6293
6198
  item.className = "comty-sidepanel-item";
6199
+ item.disabled = Boolean(comment.pending);
6294
6200
  item.innerHTML = `
6295
6201
  <span class="comty-sidepanel-item-status" aria-hidden="true"></span>
6296
6202
  <span class="comty-sidepanel-item-body">
@@ -6311,7 +6217,7 @@ function initComty(options = {}) {
6311
6217
 
6312
6218
  const metaEl = item.querySelector(".comty-sidepanel-item-meta");
6313
6219
  if (metaEl instanceof HTMLElement) {
6314
- metaEl.innerText = `${comment.author_name || "Anonymous"} · ${getCommentLabel(comment)}`;
6220
+ metaEl.innerText = `${comment.author_name || "Anonymous"} · ${comment.pending ? "Sending…" : getCommentLabel(comment)}`;
6315
6221
  metaEl.title = getCommentLabel(comment);
6316
6222
  }
6317
6223
 
@@ -6430,15 +6336,14 @@ function initComty(options = {}) {
6430
6336
  createStatusFilters();
6431
6337
 
6432
6338
  async function updateCommentStatus(comment, status) {
6433
- if (comment?.local_agent || !projectId || !comment?.id) return false;
6339
+ if (!projectId || !comment?.id) return false;
6434
6340
 
6435
6341
  let res;
6436
6342
  try {
6437
- const local = comment.local_agent === true;
6438
- res = await fetch(local ? `${agentBridgeUrl}/tasks/${encodeURIComponent(comment.id)}/status` : `${apiUrl}/api/comments/${comment.id}/status`, {
6343
+ res = await fetch(`${apiUrl}/api/comments/${comment.id}/status`, {
6439
6344
  method: "POST",
6440
- headers: local ? { "Content-Type": "application/json" } : getSdkHeaders({ "Content-Type": "application/json" }),
6441
- body: JSON.stringify(local ? { status } : {
6345
+ headers: getSdkHeaders({ "Content-Type": "application/json" }),
6346
+ body: JSON.stringify({
6442
6347
  projectKey,
6443
6348
  host: window.location.host,
6444
6349
  status,
@@ -6455,7 +6360,7 @@ function initComty(options = {}) {
6455
6360
  }
6456
6361
 
6457
6362
  const data = await res.json();
6458
- comment.status = getCommentStatus(data.task?.status ?? data.status).value;
6363
+ comment.status = getCommentStatus(data.status).value;
6459
6364
  return comment.status;
6460
6365
  }
6461
6366
 
@@ -6643,11 +6548,13 @@ function initComty(options = {}) {
6643
6548
  const openCommentThread = createCommentThread({
6644
6549
  root, apiUrl, projectKey, commentStatuses, selectedStatusFilters, placedPins,
6645
6550
  closeBubbles, getCommentStatus, updateCommentStatus, applyStatusDot,
6646
- renderSidepanelComments, applyStatusFilters, loadComments, createThreadItem,
6551
+ renderSidepanelComments, applyStatusFilters, createThreadItem, renderCommentContent,
6647
6552
  isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
6648
6553
  updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
6649
6554
  showAuthPrompt,
6650
6555
  get canWrite() { return canWrite; },
6556
+ get canManage() { return canManage; },
6557
+ get allowGuestComments() { return allowGuestComments; },
6651
6558
  get currentUser() { return currentUser; },
6652
6559
  get cachedComments() { return cachedComments; },
6653
6560
  set cachedComments(value) { cachedComments = value; },
@@ -6697,20 +6604,11 @@ function initComty(options = {}) {
6697
6604
  const x = visibility.clientX;
6698
6605
  const y = visibility.clientY;
6699
6606
 
6700
- const pin = document.createElement("button");
6701
- pin.className = "comty-pin";
6702
- pin.type = "button";
6607
+ const pin = createCommentPin(comment, x, y);
6608
+ pin.disabled = Boolean(comment.pending);
6703
6609
  pin.dataset.status = getCommentStatus(comment.status).value;
6704
- if (comment.id) {
6705
- pin.dataset.commentId = String(comment.id);
6706
- }
6707
- pin.style.left = `${x}px`;
6708
- pin.style.top = `${y}px`;
6709
- setPinIcon(pin, comment);
6710
- const pinStatusDot = document.createElement("span");
6711
- pinStatusDot.className = "comty-pin-status-dot";
6712
- applyStatusDot(pinStatusDot, comment.status);
6713
- pin.appendChild(pinStatusDot);
6610
+ if (comment.id) pin.dataset.commentId = String(comment.id);
6611
+ const pinStatusDot = pin.querySelector(".comty-pin-status-dot");
6714
6612
  pin.setAttribute(
6715
6613
  "aria-label",
6716
6614
  `Afficher le commentaire, statut ${getCommentStatus(comment.status).label}`,
@@ -6752,32 +6650,14 @@ function initComty(options = {}) {
6752
6650
  activeHoverCommentIds.add(commentId);
6753
6651
  }
6754
6652
 
6755
- const hoverBubble = document.createElement("div");
6756
- hoverBubble.className = "comty-bubble comty-hover-bubble";
6653
+ const hoverBubble = createCommentPreview(comment);
6757
6654
  hoverBubble.style.left = `${x + 32}px`;
6758
6655
  hoverBubble.style.top = `${y - 17}px`;
6759
- hoverBubble.style.pointerEvents = "none";
6760
6656
  hoverBubble.dataset.hoverId = commentId;
6761
6657
 
6762
- const hoverThread = document.createElement("div");
6763
- hoverThread.className = "comty-thread";
6764
- const hoverItem = createThreadItem(comment, { showMore: false });
6765
- hoverThread.appendChild(hoverItem.item);
6766
- hoverBubble.appendChild(hoverThread);
6767
-
6768
6658
  root.appendChild(hoverBubble);
6769
6659
 
6770
- requestAnimationFrame(() => {
6771
- if (x + 16 + hoverBubble.offsetWidth > window.innerWidth) {
6772
- hoverBubble.style.left = `${x - hoverBubble.offsetWidth - 24}px`;
6773
- hoverBubble.classList.add("comty-bubble-left");
6774
- }
6775
-
6776
- const rect = hoverBubble.getBoundingClientRect();
6777
- if (rect.bottom > window.innerHeight) {
6778
- hoverBubble.style.top = `${y - rect.height - 16}px`;
6779
- }
6780
- });
6660
+ requestAnimationFrame(() => positionCommentBubble(hoverBubble, x, y));
6781
6661
  };
6782
6662
 
6783
6663
  pin.onmouseleave = () => {
@@ -6872,12 +6752,15 @@ function initComty(options = {}) {
6872
6752
  function reconcileComments(comments) {
6873
6753
  const previous = new Map(cachedComments.map(comment => [String(comment.id), comment]));
6874
6754
  cachedComments = comments
6875
- .filter(comment => comment.kind !== "agent" || comment.status !== "done")
6755
+ .filter(comment => comment.kind !== "agent" && !comment.local_agent)
6876
6756
  .map(comment => Object.assign(previous.get(String(comment.id)) || {}, comment));
6757
+ for (const comment of previous.values()) {
6758
+ if (comment.pending && !cachedComments.some(item => item.id === comment.id)) cachedComments.push(comment);
6759
+ }
6877
6760
  }
6878
6761
 
6879
6762
  async function loadComments({ force = false } = {}) {
6880
- if (!projectId && !agent) return;
6763
+ if (!projectId) return;
6881
6764
  const pagePath = window.location.pathname;
6882
6765
  if (!force && cachedPagePath === pagePath) { await renderComments(cachedComments); return; }
6883
6766
  if (pendingCommentsLoad) {
@@ -6885,7 +6768,7 @@ function initComty(options = {}) {
6885
6768
  if (!force) return;
6886
6769
  }
6887
6770
  pendingCommentsLoad = (async () => {
6888
- let comments = cachedPagePath === pagePath ? cachedComments.filter(comment => !comment.local_agent) : [];
6771
+ let comments = cachedPagePath === pagePath ? cachedComments : [];
6889
6772
  if (projectId) {
6890
6773
  try {
6891
6774
  const url = new URL(`${apiUrl}/api/comments`);
@@ -6896,37 +6779,15 @@ function initComty(options = {}) {
6896
6779
  if (Array.isArray(data.comments)) comments = data.comments;
6897
6780
  } catch (error) { console.error("Comty: list comments failed", error); }
6898
6781
  }
6899
- if (agent) {
6900
- try {
6901
- const res = await fetch(`${agentBridgeUrl}/tasks`, { headers: agentHeaders(), cache: "no-store" });
6902
- if (!res.ok) throw new Error("Local companion unavailable");
6903
- const data = await res.json();
6904
- if (Array.isArray(data.tasks)) localTasks = data.tasks;
6905
- if (Array.isArray(data.approvals)) localApprovals = data.approvals;
6906
- agentConnectionError = "";
6907
- } catch (error) {
6908
- agentConnectionError = "Local agent disconnected. Check the development server, then reload this page. Your task history is kept.";
6909
- console.error("Comty: local tasks failed", error);
6910
- }
6911
- comments.push(...localTasks.filter(task => task.page_path === pagePath && !["done", "cancel"].includes(task.status)));
6912
- renderAgentPanel();
6913
- }
6914
6782
  reconcileComments(comments);
6915
6783
  cachedPagePath = pagePath;
6916
6784
  renderSidepanelComments();
6917
6785
  await renderComments(cachedComments);
6918
- syncAgentWorkingOutlines();
6919
6786
  ensureLiveTracking();
6920
6787
  })();
6921
6788
  try { await pendingCommentsLoad; } finally { pendingCommentsLoad = null; }
6922
6789
  }
6923
6790
 
6924
- if (agent) {
6925
- setInterval(() => {
6926
- if (!pendingCommentsLoad && document.visibilityState === "visible") void loadComments({ force: true });
6927
- }, 2000);
6928
- }
6929
-
6930
6791
  function getPinPoint(pin) {
6931
6792
  return { x: parseFloat(pin.style.left), y: parseFloat(pin.style.top) };
6932
6793
  }
@@ -6993,50 +6854,16 @@ function initComty(options = {}) {
6993
6854
  }
6994
6855
  }
6995
6856
 
6996
- function syncAgentWorkingOutlines() {
6997
- const working = localTasks.filter(task => task.status === "in_progress" && task.page_path === window.location.pathname);
6998
- const ids = new Set(working.map(task => String(task.id)));
6999
- for (const [id, outline] of workingOutlines) {
7000
- if (ids.has(id)) continue;
7001
- outline.remove(); workingOutlines.delete(id);
7002
- }
7003
- const modal = getTopActiveModal();
7004
- for (const task of working) {
7005
- const id = String(task.id);
7006
- let outline = workingOutlines.get(id);
7007
- if (!outline) {
7008
- outline = document.createElement("div");
7009
- outline.className = "comty-agent-working";
7010
- outline.setAttribute("aria-label", "Agent working on this element");
7011
- const badge = document.createElement("span");
7012
- badge.className = "comty-agent-working-badge";
7013
- badge.append(createWorkingIndicator(), "Working");
7014
- outline.appendChild(badge); root.appendChild(outline);
7015
- workingOutlines.set(id, outline);
7016
- }
7017
- const target = resolveCommentElement(task);
7018
- const point = target && getVisibleAnchorPoint(target, Number(task.rel_x), Number(task.rel_y), modal);
7019
- outline.hidden = !point;
7020
- if (!point) continue;
7021
- const rect = target.getBoundingClientRect();
7022
- setMeasuredRect(outline, rect);
7023
- outline.toggleAttribute("data-badge-below", rect.top < 32);
7024
- }
7025
- }
7026
-
7027
6857
  function onFrame() {
7028
6858
  if (motion.enabled) updateMotion();
7029
6859
  if (session.comments) syncCommentAnchors();
7030
- if (session.agent || workingOutlines.size || localTasks.some(task => task.status === "in_progress" && task.page_path === window.location.pathname)) {
7031
- syncAgentWorkingOutlines();
7032
- }
7033
6860
  if (session.inspect || session.edit || inspectionLayer.childNodes.length) renderInspector();
7034
6861
  if (session.inspect && rulersEnabled) renderRulers();
7035
6862
  if (session.inspect && rulersEnabled) measurements.render();
7036
6863
  }
7037
6864
 
7038
6865
  function ensureLiveTracking() {
7039
- const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0) || workingOutlines.size > 0;
6866
+ const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0);
7040
6867
  if (!active || document.hidden) {
7041
6868
  if (liveFrame) cancelAnimationFrame(liveFrame);
7042
6869
  liveFrame = null;
@@ -7056,14 +6883,13 @@ function initComty(options = {}) {
7056
6883
  syncOverlayHost();
7057
6884
 
7058
6885
  function renderCachedComments() {
7059
- if (!session.comments && !session.inspect && !session.edit && !workingOutlines.size) return;
6886
+ if (!session.comments && !session.inspect && !session.edit) return;
7060
6887
  if (resizeRenderFrame) {
7061
6888
  window.cancelAnimationFrame(resizeRenderFrame);
7062
6889
  }
7063
6890
  resizeRenderFrame = window.requestAnimationFrame(() => {
7064
6891
  resizeRenderFrame = null;
7065
6892
  syncCommentAnchors();
7066
- syncAgentWorkingOutlines();
7067
6893
  renderInspector();
7068
6894
  ensureLiveTracking();
7069
6895
  });
@@ -7151,7 +6977,6 @@ function initComty(options = {}) {
7151
6977
  hideInspector();
7152
6978
  return;
7153
6979
  }
7154
- if (inspectedElement !== target) inspectExpanded = selectedElement ? inspectExpanded : false;
7155
6980
  showInspector(target);
7156
6981
  }, true);
7157
6982
  document.documentElement.addEventListener("pointerleave", () => {
@@ -7208,7 +7033,6 @@ function initComty(options = {}) {
7208
7033
  }
7209
7034
  measuredRegion = null;
7210
7035
  selectedElement = getAnchorElement(target);
7211
- inspectExpanded = false;
7212
7036
  showInspector(target);
7213
7037
  return;
7214
7038
  }
@@ -7244,12 +7068,11 @@ function initComty(options = {}) {
7244
7068
  const y = e.clientY;
7245
7069
 
7246
7070
  const anchorContext = getAnchorContext(el);
7247
- if (session.agent && anchorContext) anchorContext.inspectedStyles = getInspectRows(el);
7248
7071
 
7249
7072
  const payload = {
7250
7073
  ...getDeviceContext(),
7251
7074
  projectId,
7252
- kind: session.agent ? "agent" : "comment",
7075
+ kind: "comment",
7253
7076
  host: window.location.host,
7254
7077
  pagePath: window.location.pathname,
7255
7078
  selector: getSelector(el),
@@ -7349,7 +7172,6 @@ function initComty(options = {}) {
7349
7172
  window.addEventListener("blur", () => { measureAlt = false; renderInspector(); });
7350
7173
 
7351
7174
  async function boot() {
7352
- if (localOnly) { await loadComments(); return; }
7353
7175
  const res = await fetch(`${apiUrl}/api/sdk/bootstrap`, {
7354
7176
  method: "POST",
7355
7177
  headers: getSdkHeaders({ "Content-Type": "application/json" }),
@@ -7362,20 +7184,32 @@ function initComty(options = {}) {
7362
7184
 
7363
7185
  if (!res.ok) {
7364
7186
  console.error("Comty: bootstrap failed", await res.text());
7365
- if (agent) await loadComments();
7366
7187
  return;
7367
7188
  }
7368
7189
 
7369
7190
  const data = await res.json();
7370
7191
  projectId = data.projectId;
7371
7192
  canWrite = Boolean(data.canWrite);
7193
+ canManage = Boolean(data.canManage ?? data.canWrite);
7194
+ allowGuestComments = Boolean(data.allowGuestComments);
7372
7195
  currentUser = data.user || null;
7373
7196
  loginUrl = data.loginUrl || "";
7374
7197
 
7375
7198
  if (!canWrite && sdkToken) {
7376
- sessionStorage.removeItem(tokenStorageKey);
7199
+ try { sessionStorage.removeItem(tokenStorageKey); } catch { /* Storage may be unavailable. */ }
7377
7200
  sdkToken = "";
7378
7201
  }
7202
+ if (didCompleteSignIn || authStatus === "forbidden") {
7203
+ let previousSession;
7204
+ try {
7205
+ previousSession = JSON.parse(sessionStorage.getItem(authReturnStorageKey) || "null");
7206
+ sessionStorage.removeItem(authReturnStorageKey);
7207
+ } catch { /* Fall back to the requested comment mode. */ }
7208
+ const mode = canWrite && ["browse", "edit", "inspect"].includes(previousSession?.mode)
7209
+ ? previousSession.mode : canWrite ? "edit" : "browse";
7210
+ setSession(mode, { entry: previousSession?.entry || mode });
7211
+ if (previousSession?.sidepanelOpen) setSidepanelOpen(true);
7212
+ }
7379
7213
  if (authStatus === "forbidden") showAuthPrompt(true);
7380
7214
  if (canWrite && didCompleteSignIn) {
7381
7215
  const toast = document.createElement("div");
@@ -7398,34 +7232,19 @@ function initComty(options = {}) {
7398
7232
  getDistanceSegments, getInspectRows, syncCommentAnchors,
7399
7233
  setInspectorMode(enabled) { setSession(enabled ? "inspect" : "off"); },
7400
7234
  startEditSession, startBrowseSession, endSession, showInspector,
7401
- motion, setMotionEnabled, updateMotion, describeAgentError,
7402
- previewAgent(tasks, approvals = [], error = "") {
7403
- localTasks = tasks;
7404
- localApprovals = approvals;
7405
- agentConnectionError = error;
7406
- startAgentSession();
7407
- agentPanelOpen = true;
7408
- agentPanelSignature = "";
7409
- renderAgentPanel();
7410
- },
7235
+ motion, setMotionEnabled, updateMotion,
7411
7236
  seed(comments) {
7412
7237
  reconcileComments(comments);
7413
7238
  setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
7414
7239
  return renderComments(cachedComments, { retry: false });
7415
7240
  },
7416
- seedAgentTasks(tasks) {
7417
- localTasks = tasks;
7418
- syncAgentWorkingOutlines();
7419
- ensureLiveTracking();
7420
- },
7421
- setCanWrite(value) { canWrite = value; },
7241
+ setCanWrite(value) { canWrite = value; canManage = value; },
7422
7242
  invalidate() { anchorRevision += 1; },
7423
7243
  });
7424
7244
  }
7425
7245
 
7426
7246
  return boot().catch(error => {
7427
7247
  console.error("Comty: bootstrap failed", error);
7428
- if (agent) return loadComments();
7429
7248
  });
7430
7249
  }
7431
7250
  }