@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/README.md +18 -46
- package/comty-sdk.js +1242 -1423
- package/package.json +8 -12
- package/src/browser.d.ts +1 -3
- package/src/browser.js +461 -693
- package/src/capture-engine.js +26 -0
- package/src/capture-resources.js +94 -0
- package/src/capture.js +249 -459
- package/src/comments.js +45 -32
- package/src/style.js +365 -236
- package/src/agent-process.mjs +0 -89
- package/src/claude-permission.mjs +0 -37
- package/src/claude.mjs +0 -120
- package/src/codex.mjs +0 -66
- package/src/companion.mjs +0 -207
- package/src/cursor.mjs +0 -49
- package/src/dev.mjs +0 -169
- package/src/vite.d.ts +0 -5
- package/src/vite.mjs +0 -66
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-
|
|
16
|
-
--comty-
|
|
17
|
-
--comty-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
395
|
-
height:
|
|
332
|
+
width: 28px;
|
|
333
|
+
height: 28px;
|
|
396
334
|
padding: 0;
|
|
397
335
|
border: none;
|
|
398
|
-
border-radius: 999px 999px 999px
|
|
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
|
|
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:
|
|
418
|
-
height:
|
|
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:
|
|
428
|
-
height:
|
|
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
|
|
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
|
|
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
|
|
427
|
+
box-shadow: 0 8px 24px rgba(0,0,0,.2);
|
|
490
428
|
padding: 0;
|
|
491
|
-
border-radius:
|
|
492
|
-
width: min(
|
|
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:
|
|
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:
|
|
525
|
-
min-height:
|
|
526
|
-
padding: 0
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
584
|
-
max-height: min(
|
|
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:
|
|
551
|
+
grid-template-columns: 1.5rem minmax(0, 1fr);
|
|
598
552
|
align-items: start;
|
|
599
|
-
gap:
|
|
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:
|
|
615
|
-
gap:
|
|
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:
|
|
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:
|
|
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:
|
|
641
|
-
height:
|
|
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:
|
|
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:
|
|
659
|
-
height:
|
|
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:
|
|
675
|
-
height:
|
|
628
|
+
width: 24px;
|
|
629
|
+
height: 24px;
|
|
676
630
|
flex: 0 0 auto;
|
|
677
631
|
padding: 0;
|
|
678
632
|
border: 0;
|
|
679
|
-
border-radius:
|
|
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:
|
|
710
|
-
height:
|
|
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:
|
|
718
|
-
min-height:
|
|
671
|
+
gap: 4px;
|
|
672
|
+
min-height: 24px;
|
|
719
673
|
padding: 0 8px;
|
|
720
674
|
border: 0;
|
|
721
|
-
border-radius:
|
|
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
|
|
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:
|
|
756
|
-
height:
|
|
757
|
-
margin:
|
|
709
|
+
width: 20px;
|
|
710
|
+
height: 20px;
|
|
711
|
+
margin: 0 0 0 auto;
|
|
758
712
|
}
|
|
759
713
|
|
|
760
714
|
.comty-thread-more svg {
|
|
761
|
-
width:
|
|
762
|
-
height:
|
|
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-
|
|
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-
|
|
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:
|
|
735
|
+
grid-template-columns: 1.5rem minmax(0, 1fr);
|
|
782
736
|
align-items: center;
|
|
783
|
-
gap:
|
|
784
|
-
margin-top:
|
|
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:
|
|
791
|
-
padding:
|
|
792
|
-
border-radius:
|
|
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
|
|
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:
|
|
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:
|
|
821
|
-
height:
|
|
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:
|
|
827
|
-
color:
|
|
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:
|
|
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:
|
|
850
|
-
height:
|
|
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:
|
|
859
|
-
padding: 4px
|
|
832
|
+
min-height: 32px;
|
|
833
|
+
padding: 4px 12px;
|
|
860
834
|
border: 0;
|
|
861
|
-
border-radius:
|
|
835
|
+
border-radius: 12px;
|
|
862
836
|
background: var(--comty-field-surface);
|
|
863
|
-
color:
|
|
837
|
+
color: var(--comty-foreground);
|
|
864
838
|
text-align: left;
|
|
865
|
-
font: 600
|
|
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:
|
|
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;
|
|
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(.
|
|
1197
|
-
.comty-property-row button[data-copied] svg:first-child { opacity: 0; transform: scale(.
|
|
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:
|
|
1250
|
-
padding
|
|
1251
|
-
color: var(--comty-foreground); background: var(--comty-
|
|
1252
|
-
box-shadow: var(--comty-floating-shadow); font:
|
|
1228
|
+
position: fixed; width: var(--comty-inspector-width); max-width: calc(100vw - 1rem); max-height: calc(100vh - 1rem);
|
|
1229
|
+
padding: .25rem; border-radius: .75rem; overflow-y: auto;
|
|
1230
|
+
color: var(--comty-foreground); background: var(--comty-toolbar-group);
|
|
1231
|
+
box-shadow: var(--comty-floating-shadow); font: var(--comty-property-font-size)/1.4 var(--comty-font);
|
|
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
|
|
1257
|
-
.comty-inspector-
|
|
1258
|
-
.comty-inspector-
|
|
1259
|
-
.comty-inspector-
|
|
1260
|
-
.comty-inspector-
|
|
1261
|
-
.comty-inspector
|
|
1262
|
-
.comty-inspector button
|
|
1235
|
+
.comty-inspector-head { display: flex; align-items: center; gap: .25rem; padding: 0 .125rem 0 .375rem; min-height: 1.75rem; }
|
|
1236
|
+
.comty-inspector-selector { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; color: var(--comty-muted); font-family: ui-monospace, monospace; }
|
|
1237
|
+
.comty-inspector-details { padding: .25rem .25rem .25rem .5rem; border-radius: .5rem; background: var(--comty-toolbar-surface); }
|
|
1238
|
+
.comty-inspector-row { display: grid; grid-template-columns: var(--comty-inspector-label-width) minmax(0, 1fr) var(--comty-copy-size); align-items: center; gap: .25rem; min-height: var(--comty-inspector-row-height); }
|
|
1239
|
+
.comty-inspector-label, .comty-inspector-secondary { color: var(--comty-muted); }
|
|
1240
|
+
.comty-inspector-label { overflow-wrap: anywhere; }
|
|
1241
|
+
.comty-inspector button { min-width: 0; padding: 0; border: 0; color: inherit; background: transparent; font: inherit; cursor: pointer; }
|
|
1263
1242
|
.comty-inspector button:focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .125rem; }
|
|
1264
|
-
.comty-inspector-value { display: flex;
|
|
1265
|
-
.comty-inspector
|
|
1266
|
-
.comty-inspector-
|
|
1267
|
-
.comty-inspector-
|
|
1268
|
-
.comty-inspector-
|
|
1269
|
-
.comty-inspector-
|
|
1270
|
-
.comty-inspector-
|
|
1271
|
-
.comty-inspector
|
|
1243
|
+
.comty-inspector-value { display: flex; align-items: center; gap: .3125rem; min-width: 0; padding-block: .125rem; text-align: left; font-variant-numeric: tabular-nums; }
|
|
1244
|
+
.comty-inspector button.comty-inspector-value { padding-block: .125rem; }
|
|
1245
|
+
.comty-inspector-text { min-width: 0; overflow-wrap: anywhere; }
|
|
1246
|
+
.comty-inspector-text:has(.comty-inspector-tokens) { display: flex; flex-wrap: wrap; gap: 0 .3125rem; }
|
|
1247
|
+
.comty-inspector-swatch { display: inline-block; flex: 0 0 .6875rem; width: .6875rem; height: .6875rem; border-radius: .1875rem; }
|
|
1248
|
+
.comty-inspector .comty-inspector-copy { display: grid; place-items: center; flex: 0 0 var(--comty-copy-size); width: var(--comty-copy-size); height: var(--comty-copy-size); border-radius: .375rem; }
|
|
1249
|
+
@media (hover: hover) and (pointer: fine) { .comty-inspector-copy:hover { background: var(--comty-hover-surface); } }
|
|
1250
|
+
@media (pointer: coarse) { .comty-inspector { --comty-copy-size: 2.75rem; } }
|
|
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:
|
|
1281
|
-
border-radius:
|
|
1282
|
-
width:
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
|
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-
|
|
1294
|
-
|
|
1295
|
-
|
|
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
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
|
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:
|
|
1292
|
+
overflow-y: hidden;
|
|
1313
1293
|
color: var(--comty-foreground);
|
|
1314
1294
|
scrollbar-width: thin;
|
|
1315
|
-
scrollbar-color:
|
|
1295
|
+
scrollbar-color: var(--comty-muted) transparent;
|
|
1296
|
+
transition: width var(--comty-motion-duration) var(--comty-ease-in-out), height var(--comty-motion-duration) var(--comty-ease-in-out);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
.comty-create-expanded .comty-create-textarea {
|
|
1300
|
+
width: calc(100% - 1.5rem);
|
|
1301
|
+
height: 2.5rem;
|
|
1302
|
+
overflow-y: auto;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
.comty-create-attachment {
|
|
1306
|
+
position: absolute;
|
|
1307
|
+
top: 3.25rem;
|
|
1308
|
+
left: .75rem;
|
|
1309
|
+
right: .75rem;
|
|
1310
|
+
display: none;
|
|
1311
|
+
min-width: 0;
|
|
1316
1312
|
}
|
|
1317
1313
|
|
|
1318
|
-
.comty-create-
|
|
1314
|
+
.comty-create-has-attachment .comty-create-attachment { display: flex; }
|
|
1315
|
+
.comty-create-attachment-chip {
|
|
1316
|
+
display: inline-flex;
|
|
1317
|
+
align-items: center;
|
|
1318
|
+
min-width: 0;
|
|
1319
|
+
max-width: 100%;
|
|
1320
|
+
padding: .125rem .125rem .125rem 0;
|
|
1321
|
+
border-radius: .375rem;
|
|
1322
|
+
background: var(--comty-field-surface);
|
|
1323
|
+
}
|
|
1324
|
+
.comty-create-attachment .comty-comment-link { flex: 0 1 auto; padding: 0 .5rem; line-height: 1.25rem; background: transparent; }
|
|
1325
|
+
.comty-create-attachment button {
|
|
1326
|
+
flex: 0 0 auto;
|
|
1327
|
+
width: 1.25rem;
|
|
1328
|
+
height: 1.25rem;
|
|
1329
|
+
padding: 0;
|
|
1330
|
+
border: 0;
|
|
1331
|
+
border-radius: .25rem;
|
|
1332
|
+
background: transparent;
|
|
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:
|
|
1333
|
-
height:
|
|
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:
|
|
1473
|
+
color: var(--comty-muted);
|
|
1338
1474
|
border: none;
|
|
1339
1475
|
cursor: pointer;
|
|
1340
|
-
transition: background
|
|
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
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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
|
-
|
|
1851
|
-
|
|
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
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
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
|
-
|
|
2136
|
-
|
|
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
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
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
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
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
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
2650
|
+
renderCommentContent(item.content, previousContent || "");
|
|
2619
2651
|
renderSidepanelComments();
|
|
2620
2652
|
return;
|
|
2621
2653
|
}
|
|
2622
|
-
item.content
|
|
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
|
|
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("
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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"
|
|
3099
|
-
get edit() { return this.mode === "edit"
|
|
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'
|
|
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.
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
4544
|
-
<
|
|
4545
|
-
|
|
4546
|
-
|
|
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
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
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("
|
|
4570
|
-
|
|
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 !==
|
|
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
|
|
4597
|
-
|
|
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",
|
|
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,
|
|
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
|
|
4989
|
-
const
|
|
4990
|
-
const
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
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.
|
|
5005
|
-
|
|
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
|
-
|
|
5035
|
-
|
|
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.
|
|
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 {
|
|
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
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
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(
|
|
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
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
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.
|
|
5592
|
-
|
|
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
|
|
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 =
|
|
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",
|
|
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(
|
|
5971
|
+
post = await fetch(`${apiUrl}/api/comments`, {
|
|
5887
5972
|
method: "POST",
|
|
5888
|
-
headers:
|
|
5889
|
-
body: JSON.stringify(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
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
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
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
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.
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
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.
|
|
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 (
|
|
6339
|
+
if (!projectId || !comment?.id) return false;
|
|
6434
6340
|
|
|
6435
6341
|
let res;
|
|
6436
6342
|
try {
|
|
6437
|
-
|
|
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:
|
|
6441
|
-
body: JSON.stringify(
|
|
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.
|
|
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,
|
|
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 =
|
|
6701
|
-
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
|
-
|
|
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 =
|
|
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"
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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
|
}
|