@comty.io/core 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/comty-sdk.js CHANGED
@@ -1,13 +1,16 @@
1
1
  // Generated by npm run build in sdk/. Edit src/browser.js and its imported modules instead.
2
2
  (function () {
3
3
  function getComtyStyle() {
4
+ const toolbarSurface = "#2b2d2f";
5
+ const toolbarMuted = "#b8babb";
4
6
  const commentCursor = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><path d="M20 2C30 2 38 10 38 20S30 38 20 38H3V28C2 25 2 23 2 20 2 10 10 2 20 2Z" fill="white" stroke="#d6dbe2"/><circle cx="20" cy="20" r="14" fill="#e6e6e6"/><path d="M20 14v12m-6-6h12" fill="none" stroke="#404040" stroke-width="2.5" stroke-linecap="round"/></svg>`;
7
+ const typographyCursor = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect x="8" y="8" width="23" height="23" rx="7" fill="${toolbarSurface}" stroke="${toolbarMuted}" stroke-width=".75"/><path d="m14 25 5.5-12L25 25m-8.5-4h6M13 25h4m5 0h4" fill="none" stroke="${toolbarMuted}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="m2 2 2 15 4-5 6-3Z" fill="${toolbarSurface}" stroke="${toolbarMuted}" stroke-width="1.5" stroke-linejoin="round"/></svg>`;
5
8
  return `
6
9
  #comty-root {
7
- --comty-surface: #262626;
8
- --comty-hover-surface: #393939;
10
+ --comty-surface: var(--comty-toolbar-surface);
11
+ --comty-hover-surface: var(--comty-toolbar-selected);
9
12
  --comty-foreground: #fafafa;
10
- --comty-muted: #a3a3a3;
13
+ --comty-muted: var(--comty-toolbar-muted);
11
14
  --comty-accent: #55aaf5;
12
15
  --comty-agent-warning: #e9b86d;
13
16
  --comty-agent-success: #80c9a4;
@@ -15,8 +18,8 @@ function getComtyStyle() {
15
18
  --comty-outline: oklch(0.62 0.18 255);
16
19
  --comty-paper: #fff;
17
20
  --comty-ink: #171717;
18
- --comty-border: #e5e5e5;
19
- --comty-field-surface: #f5f5f5;
21
+ --comty-border: #434649;
22
+ --comty-field-surface: var(--comty-toolbar-group);
20
23
  --comty-xray: #0000ff;
21
24
  --comty-guide: #ff0000;
22
25
  --comty-margin-fill: rgb(246 172 88 / .45);
@@ -25,15 +28,48 @@ function getComtyStyle() {
25
28
  --comty-floating-shadow: 0 .25rem 1rem rgb(0 0 0 / .16);
26
29
  --comty-tool-size: 2.375rem;
27
30
  --comty-toolbar-padding: .375rem;
31
+ --comty-toolbar-gap: .375rem;
32
+ --comty-toolbar-grip-width: .6rem;
33
+ --comty-toolbar-grip-gap: .25rem;
34
+ --comty-magnifier-size: 5.375rem;
35
+ --comty-magnifier-rim: .625rem;
36
+ --comty-magnifier-line: .125rem;
37
+ --comty-magnifier-grid-line: .03125rem;
38
+ --comty-magnifier-dark: #111;
39
+ --comty-magnifier-gray: #999;
40
+ --comty-magnifier-shadow: 0 .0625rem .375rem rgb(0 0 0 / .5);
41
+ --comty-property-width: 16rem;
42
+ --comty-property-swatch: 2rem;
43
+ --comty-property-font-size: .6875rem;
44
+ --comty-property-row-height: 1.375rem;
45
+ --comty-toolbar-surface: ${toolbarSurface};
46
+ --comty-toolbar-group: #202224;
47
+ --comty-toolbar-selected: #505457;
48
+ --comty-toolbar-muted: ${toolbarMuted};
49
+ --comty-measure: #ff3b30;
50
+ --comty-font: "Inter", ui-sans-serif, system-ui, sans-serif;
51
+ --comty-dialog-surface: var(--comty-surface);
52
+ --comty-dialog-ink: var(--comty-foreground);
53
+ --comty-dialog-soft: var(--comty-muted);
54
+ --comty-dialog-field: var(--comty-field-surface);
55
+ --comty-dialog-line: var(--comty-border);
56
+ --comty-dialog-backdrop: rgb(0 0 0 / .4);
57
+ --comty-dialog-radius: 2rem;
58
+ --comty-dialog-width: 32rem;
59
+ --comty-dialog-shadow: 0 1.5rem 4rem rgb(0 0 0 / .16);
28
60
  --comty-motion-duration: 220ms;
61
+ --comty-working-dot-size: .25em;
62
+ --comty-working-dot-gap: .125em;
29
63
  --comty-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
64
+ --comty-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
30
65
  position: fixed;
31
66
  inset: 0;
32
67
  width: 100%; height: 100%; max-width: none; max-height: none;
33
68
  margin: 0; padding: 0; border: 0; overflow: visible; background: transparent;
34
69
  z-index: 2147483647;
35
70
  pointer-events: none;
36
- font-family: ui-sans-serif, system-ui, sans-serif;
71
+ font-family: var(--comty-font);
72
+ color-scheme: dark;
37
73
  }
38
74
  html.comty-agent-enabled body { transition: margin-left 220ms cubic-bezier(0.23, 1, 0.32, 1), margin-right 220ms cubic-bezier(0.23, 1, 0.32, 1); }
39
75
  html.comty-agent-docked[data-comty-agent-side="left"] body { margin-left: 22rem; }
@@ -59,7 +95,9 @@ function getComtyStyle() {
59
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); }
60
96
  .comty-agent-row:hover { background: var(--comty-hover-surface); }
61
97
  .comty-agent-dot { width: .4375rem; height: .4375rem; flex: 0 0 auto; border-radius: 50%; background: var(--comty-muted); }
62
- .comty-agent-dot[data-status="working"] { background: var(--comty-accent); animation: comty-agent-pulse 1.4s ease-in-out infinite; }
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; }
63
101
  .comty-agent-dot[data-status="success"] { background: var(--comty-agent-success); }
64
102
  .comty-agent-dot[data-status="error"] { background: var(--comty-agent-error); }
65
103
  .comty-agent-row-name { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
@@ -69,7 +107,7 @@ function getComtyStyle() {
69
107
  @media (hover: none) { .comty-agent-row-actions { position: static; opacity: 1; pointer-events: auto; transform: none; background: transparent; } }
70
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; }
71
109
  .comty-agent-panel .comty-agent-row-actions .comty-agent-action:hover { color: var(--comty-foreground); background: var(--comty-surface); }
72
- .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: .0625rem solid var(--comty-hover-surface); 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; }
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; }
73
111
  .comty-agent-error-popover strong { display: block; font-weight: 600; }
74
112
  .comty-agent-error-popover p { margin: .375rem 0 0; color: var(--comty-muted); white-space: pre-wrap; overflow-wrap: anywhere; }
75
113
  .comty-agent-panel p { margin: .5rem 0 0; font: inherit; color: var(--comty-muted); }
@@ -93,10 +131,8 @@ function getComtyStyle() {
93
131
  .comty-agent-empty strong { font-size: .875rem; font-weight: 500; }
94
132
  .comty-agent-empty p { max-width: 15rem; margin: .5rem auto 0; font-size: .875rem; }
95
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; }
96
- .comty-agent-working span { position: absolute; left: -.125rem; bottom: 100%; padding: .25rem .5rem; border-radius: .375rem .375rem 0 0; background: var(--comty-accent); color: var(--comty-paper); font: 600 .6875rem/1.4 ui-sans-serif, system-ui, sans-serif; white-space: nowrap; }
97
- .comty-agent-working[data-badge-below] span { bottom: auto; top: 100%; border-radius: 0 0 .375rem .375rem; }
98
- .comty-agent-working span::before { content: ""; display: inline-block; width: .4rem; height: .4rem; margin-right: .4rem; border-radius: 50%; background: currentColor; animation: comty-agent-pulse 1.4s ease-in-out infinite; }
99
- @keyframes comty-agent-pulse { 50% { opacity: .3; } }
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; }
100
136
  #comty-root { cursor: default; }
101
137
  #comty-root::backdrop { background: transparent; pointer-events: none; }
102
138
  html.comty-comment-placement,
@@ -114,22 +150,24 @@ function getComtyStyle() {
114
150
  z-index: 2147483647;
115
151
  display: flex;
116
152
  align-items: center;
117
- gap: .125rem;
153
+ gap: var(--comty-toolbar-gap);
118
154
  max-width: calc(100vw - 2rem);
119
155
  padding: var(--comty-toolbar-padding);
120
- border-radius: .9375rem;
121
- color: var(--comty-foreground);
156
+ border-radius: 1.125rem;
157
+ color: var(--comty-toolbar-muted);
122
158
  opacity: 0;
123
159
  visibility: hidden;
124
160
  pointer-events: none;
125
161
  touch-action: none;
126
162
  user-select: none;
127
- cursor: pointer;
163
+ cursor: default;
128
164
  isolation: isolate;
129
165
  transition: opacity 180ms ease, visibility 0s linear 180ms;
130
166
  }
131
167
  #comty-root.comty-active .comty-toolbar { opacity: 1; visibility: visible; pointer-events: auto; transition-delay: 0s; }
132
- .comty-toolbar-surface { position: absolute; inset: 0; z-index: -1; border-radius: inherit; background: var(--comty-surface); box-shadow: var(--comty-floating-shadow); transform-origin: top left; pointer-events: none; }
168
+ .comty-toolbar-surface { position: absolute; inset: 0; z-index: -1; border-radius: inherit; background: var(--comty-toolbar-surface); box-shadow: var(--comty-floating-shadow); transform-origin: top left; pointer-events: none; }
169
+ .comty-toolbar-grip { display: flex; align-items: center; justify-content: center; flex: 0 0 auto; width: var(--comty-toolbar-grip-width); height: var(--comty-tool-size); margin-inline-end: calc(var(--comty-toolbar-grip-gap) - var(--comty-toolbar-gap)); color: inherit; cursor: grab; }
170
+ .comty-toolbar-grip svg { width: 1.125rem; height: 1.125rem; flex-shrink: 0; }
133
171
  .comty-toolbar[data-dragging], .comty-toolbar[data-dragging] * { cursor: grabbing; }
134
172
  .comty-toolbar:focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .125rem; }
135
173
  .comty-control-button {
@@ -153,13 +191,14 @@ function getComtyStyle() {
153
191
  }
154
192
  .comty-control-button svg { width: 1.125rem; height: 1.125rem; }
155
193
  .comty-control-button:hover { background: var(--comty-hover-surface); }
156
- .comty-control-button[aria-pressed="true"] { background: var(--comty-accent); color: var(--comty-paper); }
194
+ .comty-control-button[aria-pressed="true"] { background: var(--comty-toolbar-selected); color: var(--comty-foreground); }
157
195
  .comty-control-button:focus-visible, .comty-filter-dot:focus-visible {
158
- outline: .125rem solid var(--comty-accent);
196
+ outline: .125rem solid var(--comty-foreground);
159
197
  outline-offset: .125rem;
160
198
  }
161
- .comty-tool-context { display: flex; align-items: center; gap: .125rem; }
162
- .comty-tool-divider { width: .0625rem; height: 1.25rem; margin-inline: .375rem; background: var(--comty-hover-surface); }
199
+ .comty-tool-primary, .comty-tool-context { display: flex; align-items: center; gap: .125rem; padding: .125rem; border-radius: .875rem; background: var(--comty-toolbar-group); }
200
+ .comty-tool-primary { position: relative; z-index: 1; }
201
+ .comty-tool-divider { display: none; }
163
202
  .comty-filter-controls { display: flex; align-items: center; gap: .375rem; padding-inline: .5rem; }
164
203
  .comty-filter-dot { flex-shrink: 0; width: .625rem; height: .625rem; padding: 0; border: 0; border-radius: 50%; cursor: pointer; }
165
204
  .comty-filter-dot-muted { opacity: .35; }
@@ -167,7 +206,7 @@ function getComtyStyle() {
167
206
  [data-comty-anchor-hidden] { visibility: hidden !important; pointer-events: none !important; }
168
207
  @media (prefers-reduced-motion: reduce) {
169
208
  html.comty-agent-enabled body { transition: none; }
170
- .comty-toolbar, .comty-control-button, .comty-sidepanel, .comty-pin, .comty-bubble, .comty-agent-panel, .comty-agent-action, .comty-agent-dot, .comty-agent-working span::before { transition: none !important; animation: none !important; }
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; }
171
210
  }
172
211
 
173
212
  .comty-sidepanel {
@@ -182,9 +221,9 @@ function getComtyStyle() {
182
221
  display: flex;
183
222
  flex-direction: column;
184
223
  overflow: hidden;
185
- background: white;
186
- color: #111;
187
- border: 1px solid rgba(17,24,39,.12);
224
+ background: var(--comty-surface);
225
+ color: var(--comty-foreground);
226
+ border: 1px solid var(--comty-border);
188
227
  border-radius: 18px;
189
228
  box-shadow: 0 1.5rem 4rem rgb(23 32 29 / 0.12);
190
229
  font-family: sans-serif;
@@ -207,7 +246,7 @@ function getComtyStyle() {
207
246
  align-items: center;
208
247
  gap: 12px;
209
248
  padding: 8px;
210
- border-bottom: 1px solid rgba(17,24,39,.1);
249
+ border-bottom: 1px solid var(--comty-border);
211
250
  }
212
251
 
213
252
  .comty-sidepanel-header-filters {
@@ -224,15 +263,15 @@ function getComtyStyle() {
224
263
  height: 32px;
225
264
  border: none;
226
265
  border-radius: 8px;
227
- background: #f4f4f5;
228
- color: #111;
266
+ background: var(--comty-field-surface);
267
+ color: var(--comty-foreground);
229
268
  padding: 0 12px;
230
269
  font: 14px/1 sans-serif;
231
270
  outline: none;
232
271
  }
233
272
 
234
273
  .comty-sidepanel-search::placeholder {
235
- color: #9ca3af;
274
+ color: var(--comty-muted);
236
275
  }
237
276
 
238
277
  .comty-sidepanel-search::-webkit-search-cancel-button {
@@ -251,14 +290,14 @@ function getComtyStyle() {
251
290
  border: none;
252
291
  border-radius: 8px;
253
292
  background: transparent;
254
- color: #111;
293
+ color: var(--comty-foreground);
255
294
  cursor: pointer;
256
295
  appearance: none;
257
296
  }
258
297
 
259
298
  .comty-sidepanel-icon-button:hover,
260
299
  .comty-sidepanel-icon-button:focus-visible {
261
- background: #f4f4f5;
300
+ background: var(--comty-field-surface);
262
301
  outline: none;
263
302
  }
264
303
 
@@ -284,7 +323,7 @@ function getComtyStyle() {
284
323
  .comty-sidepanel-empty {
285
324
  display: flex;
286
325
  gap: 14px;
287
- color: #111;
326
+ color: var(--comty-foreground);
288
327
  padding: 22px 12px;
289
328
  font: 14px/1.45 sans-serif;
290
329
  }
@@ -304,7 +343,7 @@ function getComtyStyle() {
304
343
  border: none;
305
344
  border-radius: 10px;
306
345
  background: transparent;
307
- color: #111;
346
+ color: var(--comty-foreground);
308
347
  font: 14px/1.35 sans-serif;
309
348
  text-align: left;
310
349
  cursor: pointer;
@@ -314,7 +353,7 @@ function getComtyStyle() {
314
353
 
315
354
  .comty-sidepanel-item:hover,
316
355
  .comty-sidepanel-item:focus-visible {
317
- background: #f4f4f5;
356
+ background: var(--comty-field-surface);
318
357
  outline: none;
319
358
  }
320
359
 
@@ -342,7 +381,7 @@ function getComtyStyle() {
342
381
  .comty-sidepanel-item-meta {
343
382
  display: block;
344
383
  margin-top: 6px;
345
- color: #6b7280;
384
+ color: var(--comty-muted);
346
385
  font: 11px/1.2 sans-serif;
347
386
  overflow: hidden;
348
387
  text-overflow: ellipsis;
@@ -394,9 +433,9 @@ function getComtyStyle() {
394
433
  }
395
434
 
396
435
  .comty-pin.comty-pin-with-avatar {
397
- background: #fff;
398
- color: #1f1f1f;
399
- border: 2px solid #fff;
436
+ background: var(--comty-surface);
437
+ color: var(--comty-foreground);
438
+ border: 2px solid var(--comty-border);
400
439
  box-shadow: 0 2px 6px rgba(0,0,0,.18);
401
440
  filter: none;
402
441
  transition:
@@ -444,9 +483,9 @@ function getComtyStyle() {
444
483
  .comty-bubble {
445
484
  position: fixed;
446
485
  z-index: 2147483646;
447
- background: white;
448
- color: #1f1f1f;
449
- border: 1px solid rgba(0,0,0,.08);
486
+ background: var(--comty-surface);
487
+ color: var(--comty-foreground);
488
+ border: 0;
450
489
  box-shadow: 0 10px 28px rgba(0,0,0,.2);
451
490
  padding: 0;
452
491
  border-radius: 14px;
@@ -485,12 +524,12 @@ function getComtyStyle() {
485
524
  gap: 12px;
486
525
  min-height: 40px;
487
526
  padding: 0 16px;
488
- border-bottom: 1px solid rgba(0,0,0,.1);
527
+ border-bottom: 1px solid var(--comty-border);
489
528
  }
490
529
 
491
530
  .comty-bubble-title {
492
531
  margin: 0;
493
- color: #1f1f1f;
532
+ color: var(--comty-foreground);
494
533
  font-size: 12px;
495
534
  font-weight: 600;
496
535
  line-height: 1;
@@ -505,7 +544,7 @@ function getComtyStyle() {
505
544
 
506
545
  .comty-bubble-content {
507
546
  flex: 1;
508
- color: #1f1f1f;
547
+ color: var(--comty-foreground);
509
548
  font-size: 14px;
510
549
  line-height: 1.45;
511
550
  overflow-wrap: anywhere;
@@ -522,7 +561,7 @@ function getComtyStyle() {
522
561
  border: none;
523
562
  padding: 0;
524
563
  background: transparent;
525
- color: #1f1f1f;
564
+ color: var(--comty-foreground);
526
565
  cursor: text;
527
566
  font: 14px/1.45 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
528
567
  outline: none;
@@ -547,7 +586,7 @@ function getComtyStyle() {
547
586
  overflow-y: auto;
548
587
  overscroll-behavior: contain;
549
588
  scrollbar-width: thin;
550
- scrollbar-color: rgba(0,0,0,.22) transparent;
589
+ scrollbar-color: var(--comty-hover-surface) transparent;
551
590
  box-sizing: border-box;
552
591
  }
553
592
 
@@ -579,7 +618,7 @@ function getComtyStyle() {
579
618
  .comty-comment-author-name {
580
619
  min-width: 0;
581
620
  overflow: hidden;
582
- color: #1f1f1f;
621
+ color: var(--comty-foreground);
583
622
  font-size: 14px;
584
623
  font-weight: 600;
585
624
  line-height: 1.25;
@@ -589,7 +628,7 @@ function getComtyStyle() {
589
628
 
590
629
  .comty-comment-time {
591
630
  flex: 0 0 auto;
592
- color: #b3b3b3;
631
+ color: var(--comty-muted);
593
632
  font-size: 13px;
594
633
  font-weight: 400;
595
634
  line-height: 1.25;
@@ -602,8 +641,8 @@ function getComtyStyle() {
602
641
  height: 24px;
603
642
  overflow: hidden;
604
643
  border-radius: 50%;
605
- background: #e6e6e6;
606
- color: #404040;
644
+ background: var(--comty-field-surface);
645
+ color: var(--comty-foreground);
607
646
  font-size: 10px;
608
647
  font-weight: 600;
609
648
  line-height: 1;
@@ -639,7 +678,7 @@ function getComtyStyle() {
639
678
  border: 0;
640
679
  border-radius: 6px;
641
680
  background: transparent;
642
- color: #1f1f1f;
681
+ color: var(--comty-foreground);
643
682
  cursor: pointer;
644
683
  appearance: none;
645
684
  transition:
@@ -649,7 +688,7 @@ function getComtyStyle() {
649
688
 
650
689
  .comty-icon-button:hover,
651
690
  .comty-icon-button:focus-visible {
652
- background: #f2f2f2;
691
+ background: var(--comty-hover-surface);
653
692
  outline: none;
654
693
  }
655
694
 
@@ -658,7 +697,7 @@ function getComtyStyle() {
658
697
  }
659
698
 
660
699
  .comty-icon-button:disabled {
661
- color: #b3b3b3;
700
+ color: var(--comty-muted);
662
701
  cursor: default;
663
702
  }
664
703
 
@@ -681,7 +720,7 @@ function getComtyStyle() {
681
720
  border: 0;
682
721
  border-radius: 6px;
683
722
  background: transparent;
684
- color: #1f1f1f;
723
+ color: var(--comty-foreground);
685
724
  cursor: pointer;
686
725
  appearance: none;
687
726
  font: 600 12px/1 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
@@ -692,7 +731,7 @@ function getComtyStyle() {
692
731
 
693
732
  .comty-status-trigger:hover,
694
733
  .comty-status-trigger:focus-visible {
695
- background: #f2f2f2;
734
+ background: var(--comty-hover-surface);
696
735
  outline: none;
697
736
  }
698
737
 
@@ -701,7 +740,7 @@ function getComtyStyle() {
701
740
  }
702
741
 
703
742
  .comty-status-trigger:disabled {
704
- color: #8c8c8c;
743
+ color: var(--comty-muted);
705
744
  cursor: default;
706
745
  }
707
746
 
@@ -728,12 +767,12 @@ function getComtyStyle() {
728
767
  }
729
768
 
730
769
  .comty-popover-item.comty-popover-item-destructive {
731
- color: #dc2626;
770
+ color: var(--comty-agent-error);
732
771
  }
733
772
 
734
773
  .comty-popover-item.comty-popover-item-destructive:hover,
735
774
  .comty-popover-item.comty-popover-item-destructive:focus-visible {
736
- background: #fef2f2;
775
+ background: color-mix(in srgb, var(--comty-agent-error) 12%, var(--comty-surface));
737
776
  outline: none;
738
777
  }
739
778
 
@@ -751,7 +790,7 @@ function getComtyStyle() {
751
790
  min-height: 36px;
752
791
  padding: 4px 8px 4px 16px;
753
792
  border-radius: 14px;
754
- background: #f5f5f5;
793
+ background: var(--comty-field-surface);
755
794
  box-sizing: border-box;
756
795
  transition: box-shadow 160ms ease;
757
796
  }
@@ -766,13 +805,13 @@ function getComtyStyle() {
766
805
  padding: 0;
767
806
  border: 0;
768
807
  background: transparent;
769
- color: #1f1f1f;
808
+ color: var(--comty-foreground);
770
809
  font: 14px/1.3 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
771
810
  outline: none;
772
811
  }
773
812
 
774
813
  .comty-reply-input::placeholder {
775
- color: #b3b3b3;
814
+ color: var(--comty-muted);
776
815
  }
777
816
 
778
817
  .comty-reply-button {
@@ -802,7 +841,7 @@ function getComtyStyle() {
802
841
  }
803
842
 
804
843
  .comty-reply-button:disabled {
805
- background: #d9d9d9;
844
+ background: var(--comty-hover-surface);
806
845
  cursor: default;
807
846
  }
808
847
 
@@ -820,7 +859,7 @@ function getComtyStyle() {
820
859
  padding: 4px 16px;
821
860
  border: 0;
822
861
  border-radius: 14px;
823
- background: #f5f5f5;
862
+ background: var(--comty-field-surface);
824
863
  color: #0d99ff;
825
864
  text-align: left;
826
865
  font: 600 14px/1.3 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
@@ -828,7 +867,7 @@ function getComtyStyle() {
828
867
  transition: background-color 160ms ease;
829
868
  }
830
869
 
831
- .comty-sign-in-reply:hover { background: #ececec; }
870
+ .comty-sign-in-reply:hover { background: var(--comty-hover-surface); }
832
871
  .comty-sign-in-reply:focus-visible { outline: 2px solid #0d99ff; outline-offset: 2px; }
833
872
 
834
873
  .comty-hover-bubble {
@@ -872,15 +911,32 @@ function getComtyStyle() {
872
911
  font-size: 12px;
873
912
  }
874
913
 
875
- .comty-auth-backdrop { position: fixed; inset: 0; z-index: 2147483647; display: grid; place-items: center; padding: 20px; background: rgba(0,0,0,.35); font-family: sans-serif; }
876
- .comty-auth-dialog { width: min(360px, 100%); padding: 20px; border: 1px solid #ddd; border-radius: 12px; background: #fff; color: #111; box-shadow: 0 16px 48px rgba(0,0,0,.2); }
877
- .comty-auth-dialog h2 { margin: 0; font-size: 18px; line-height: 1.2; }
878
- .comty-auth-dialog p { margin: 7px 0 0; color: #6b7280; font-size: 14px; line-height: 1.45; }
879
- .comty-auth-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
880
- .comty-auth-actions button { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 7px; background: #fff; color: #111; font: 600 13px/1 sans-serif; cursor: pointer; }
881
- .comty-auth-actions .comty-auth-primary { border-color: #111; background: #111; color: #fff; }
882
- .comty-auth-actions button:disabled { opacity: .6; cursor: wait; }
883
- .comty-auth-toast { position: fixed; top: 16px; left: 50%; z-index: 2147483647; transform: translateX(-50%); padding: 9px 12px; border: 1px solid #ddd; border-radius: 8px; background: #fff; color: #111; box-shadow: 0 8px 24px rgba(0,0,0,.15); font: 600 12px/1 sans-serif; }
914
+ .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
+ .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
+ .comty-auth-dialog *, .comty-auth-dialog *::before, .comty-auth-dialog *::after { box-sizing: border-box; }
917
+ .comty-auth-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
918
+ .comty-auth-icon, .comty-auth-close { display: flex; align-items: center; justify-content: center; width: 2.5rem; height: 2.5rem; padding: 0; border: 0; background: var(--comty-dialog-field); color: var(--comty-dialog-ink); }
919
+ .comty-auth-icon { border-radius: .75rem; }
920
+ .comty-auth-close { border-radius: 50%; cursor: pointer; }
921
+ .comty-auth-header svg { width: 1.25rem; height: 1.25rem; }
922
+ .comty-auth-dialog h2 { margin: 0; color: inherit; font: 600 1.5rem/1.25 var(--comty-font); letter-spacing: -.025em; }
923
+ .comty-auth-dialog p { margin: 1rem 0 0; color: var(--comty-dialog-soft); font: 400 .875rem/1.65 var(--comty-font); }
924
+ .comty-auth-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; margin-top: 1.75rem; padding-top: 1.5rem; border-top: .0625rem solid var(--comty-dialog-line); }
925
+ .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
+ .comty-auth-actions button:hover, .comty-auth-close:hover { background: var(--comty-dialog-field); color: var(--comty-dialog-ink); }
927
+ .comty-auth-actions .comty-auth-primary { border-color: transparent; background: var(--comty-dialog-ink); color: var(--comty-dialog-surface); }
928
+ .comty-auth-actions .comty-auth-primary:hover { background: var(--comty-dialog-soft); color: var(--comty-dialog-surface); }
929
+ .comty-auth-dialog button:focus-visible { outline: .125rem solid var(--comty-dialog-ink); outline-offset: .1875rem; }
930
+ .comty-auth-dialog button:disabled { opacity: .5; cursor: wait; }
931
+ .comty-distance-layer { position: fixed; inset: 0; z-index: 1; width: 100%; height: 100%; overflow: visible; pointer-events: none; color: var(--comty-measure); }
932
+ .comty-distance-line, .comty-distance-extension { stroke: currentColor; stroke-width: 1; fill: none; shape-rendering: crispEdges; }
933
+ .comty-distance-extension { stroke-dasharray: 3 3; opacity: .65; }
934
+ .comty-distance-arrow, .comty-distance-label, .comty-distance-snap { fill: currentColor; }
935
+ .comty-distance-value { fill: var(--comty-paper); font: 500 .75rem var(--comty-font); }
936
+ .comty-distance-announcement { position: absolute; width: .0625rem; height: .0625rem; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
937
+ html.comty-measure-vertical, html.comty-measure-vertical body *:not(#comty-root):not(#comty-root *) { cursor: ns-resize !important; }
938
+ html.comty-measure-horizontal, html.comty-measure-horizontal body *:not(#comty-root):not(#comty-root *) { cursor: ew-resize !important; }
939
+ .comty-auth-toast { position: fixed; top: 16px; left: 50%; z-index: 2147483647; transform: translateX(-50%); padding: 9px 12px; border: 0; border-radius: 8px; background: var(--comty-surface); color: var(--comty-foreground); box-shadow: 0 8px 24px rgba(0,0,0,.15); font: 600 12px/1 sans-serif; }
884
940
  .comty-auth-toast button { margin-left: .5rem; padding: .375rem .5rem; border: 0; border-radius: .375rem; background: var(--comty-accent); color: var(--comty-paper); cursor: pointer; font: inherit; }
885
941
 
886
942
  .comty-close {
@@ -893,7 +949,7 @@ function getComtyStyle() {
893
949
  border: none;
894
950
  border-radius: 6px;
895
951
  background: transparent;
896
- color: #555;
952
+ color: var(--comty-muted);
897
953
  cursor: pointer;
898
954
  appearance: none;
899
955
  font: 18px/1 sans-serif;
@@ -901,8 +957,8 @@ function getComtyStyle() {
901
957
 
902
958
  .comty-close:hover,
903
959
  .comty-close:focus-visible {
904
- background: #f3f4f6;
905
- color: #111;
960
+ background: var(--comty-field-surface);
961
+ color: var(--comty-foreground);
906
962
  outline: none;
907
963
  }
908
964
 
@@ -919,10 +975,10 @@ function getComtyStyle() {
919
975
  max-width: 100%;
920
976
  min-height: 18px;
921
977
  padding: 2px 7px;
922
- border: 1px solid #e5e7eb;
978
+ border: 0;
923
979
  border-radius: 6px;
924
- background: #f8fafc;
925
- color: #475569;
980
+ background: var(--comty-field-surface);
981
+ color: var(--comty-muted);
926
982
  font-size: 11px;
927
983
  font-weight: 500;
928
984
  line-height: 1;
@@ -938,11 +994,11 @@ function getComtyStyle() {
938
994
  gap: 8px;
939
995
  margin-top: 12px;
940
996
  padding-top: 10px;
941
- border-top: 1px solid #eef2f7;
997
+ border-top: 1px solid var(--comty-border);
942
998
  }
943
999
 
944
1000
  .comty-status-label {
945
- color: #64748b;
1001
+ color: var(--comty-muted);
946
1002
  font-size: 11px;
947
1003
  font-weight: 600;
948
1004
  }
@@ -967,10 +1023,10 @@ function getComtyStyle() {
967
1023
  align-items: center;
968
1024
  gap: 6px;
969
1025
  min-height: 30px;
970
- border: 1px solid #e5e7eb;
1026
+ border: 0;
971
1027
  border-radius: 7px;
972
- background: white;
973
- color: #111827;
1028
+ background: var(--comty-surface);
1029
+ color: var(--comty-foreground);
974
1030
  padding: 4px 8px;
975
1031
  font: 600 12px/1.2 sans-serif;
976
1032
  cursor: pointer;
@@ -978,8 +1034,8 @@ function getComtyStyle() {
978
1034
 
979
1035
  .comty-status-button:hover,
980
1036
  .comty-status-button:focus-visible {
981
- border-color: #d1d5db;
982
- background: #f9fafb;
1037
+ border-color: var(--comty-muted);
1038
+ background: var(--comty-hover-surface);
983
1039
  outline: none;
984
1040
  }
985
1041
 
@@ -1002,8 +1058,8 @@ function getComtyStyle() {
1002
1058
  .comty-popover {
1003
1059
  position: fixed;
1004
1060
  z-index: 2147483646;
1005
- background: white;
1006
- border: 1px solid rgba(0,0,0,0.05);
1061
+ background: var(--comty-surface);
1062
+ border: 0;
1007
1063
  box-shadow: 0 4px 20px rgba(0,0,0,.1);
1008
1064
  border-radius: 8px;
1009
1065
  padding: 4px;
@@ -1030,7 +1086,7 @@ function getComtyStyle() {
1030
1086
  border-radius: 6px;
1031
1087
  cursor: pointer;
1032
1088
  font-size: 13px;
1033
- color: #111827;
1089
+ color: var(--comty-foreground);
1034
1090
  text-align: left;
1035
1091
  position: relative;
1036
1092
  }
@@ -1040,25 +1096,25 @@ function getComtyStyle() {
1040
1096
  right: 8px;
1041
1097
  width: 14px;
1042
1098
  height: 14px;
1043
- color: #111827;
1099
+ color: var(--comty-foreground);
1044
1100
  }
1045
1101
 
1046
1102
  .comty-popover-item:hover {
1047
- background: #f3f4f6;
1103
+ background: var(--comty-field-surface);
1048
1104
  }
1049
1105
 
1050
1106
  .comty-popover-header {
1051
1107
  padding: 6px 8px;
1052
1108
  font-size: 11px;
1053
1109
  font-weight: 600;
1054
- color: #6b7280;
1110
+ color: var(--comty-muted);
1055
1111
  text-transform: uppercase;
1056
1112
  letter-spacing: 0.05em;
1057
1113
  }
1058
1114
 
1059
1115
  .comty-popover-divider {
1060
1116
  height: 1px;
1061
- background: #e5e7eb;
1117
+ background: var(--comty-border);
1062
1118
  margin: 4px 0;
1063
1119
  }
1064
1120
 
@@ -1080,9 +1136,14 @@ function getComtyStyle() {
1080
1136
  .comty-layout-guide { display: grid; box-sizing: border-box; }
1081
1137
  .comty-layout-guide > span { background: var(--guide-color); min-width: 0; min-height: 0; }
1082
1138
  .comty-layout-grid { background-image: linear-gradient(to right, var(--guide-color) 1px, transparent 1px), linear-gradient(to bottom, var(--guide-color) 1px, transparent 1px); }
1083
- .comty-tool-panel { position: fixed; right: 1rem; bottom: 5rem; width: 18rem; max-width: calc(100vw - 2rem); max-height: calc(100vh - 7rem); overflow: auto; box-sizing: border-box; padding: .75rem; border: .0625rem solid var(--comty-border); border-radius: .75rem; background: var(--comty-paper); color: var(--comty-ink); box-shadow: var(--comty-floating-shadow); font: .8125rem/1.5 ui-sans-serif, system-ui, sans-serif; pointer-events: auto; z-index: 2147483647; }
1139
+ .comty-tool-panel { position: fixed; right: 1rem; bottom: 5rem; width: 18rem; max-width: calc(100vw - 2rem); max-height: calc(100vh - 7rem); overflow: auto; box-sizing: border-box; padding: .75rem; border: 0; border-radius: .75rem; background: var(--comty-surface); color: var(--comty-foreground); box-shadow: var(--comty-floating-shadow); font: .8125rem/1.5 ui-sans-serif, system-ui, sans-serif; pointer-events: auto; z-index: 2147483647; }
1084
1140
  .comty-tool-panel-header, .comty-guide-row { display: flex; align-items: center; gap: .5rem; }
1085
1141
  .comty-tool-panel-header { margin-bottom: .5rem; }
1142
+ .comty-tool-panel .comty-guide-select { justify-content: space-between; gap: .75rem; min-width: 6.5rem; padding: .375rem .5rem; border: 0; border-radius: .375rem; background: var(--comty-field-surface); }
1143
+ .comty-guide-select:focus-visible, .comty-guide-popover .comty-popover-item:focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .125rem; }
1144
+ .comty-guide-popover .comty-popover-item { justify-content: flex-start; padding: .375rem 2rem .375rem .5rem; }
1145
+ .comty-guide-popover .comty-popover-item-check svg { width: 100%; height: 100%; }
1146
+ .comty-guide-align { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
1086
1147
  .comty-tool-panel-header strong { flex: 1; white-space: nowrap; }
1087
1148
  .comty-tool-panel button { display: inline-flex; align-items: center; justify-content: center; gap: .5rem; padding: .375rem; border: 0; border-radius: .375rem; color: inherit; background: transparent; font: inherit; cursor: pointer; }
1088
1149
  .comty-tool-panel button:hover { background: var(--comty-border); }
@@ -1093,9 +1154,9 @@ function getComtyStyle() {
1093
1154
  .comty-guide-item + .comty-guide-item { margin-top: .375rem; }
1094
1155
  .comty-guide-fields { display: grid; gap: .5rem; margin-block: .5rem; padding: .5rem; border-radius: .375rem; background: var(--comty-field-surface); }
1095
1156
  .comty-guide-fields label { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
1096
- .comty-tool-panel input, .comty-tool-panel select { min-width: 0; width: 6rem; box-sizing: border-box; border: .0625rem solid var(--comty-border); border-radius: .25rem; padding: .25rem; background: var(--comty-paper); color: var(--comty-ink); font: inherit; }
1157
+ .comty-tool-panel input, .comty-tool-panel select { min-width: 0; width: 6rem; box-sizing: border-box; border: 0; border-radius: .25rem; padding: .25rem; background: var(--comty-surface); color: var(--comty-foreground); font: inherit; }
1097
1158
  .comty-tool-panel input[type="color"] { height: 1.75rem; padding: .125rem; }
1098
- .comty-ruler { position: fixed; background: var(--comty-paper); pointer-events: auto; touch-action: none; }
1159
+ .comty-ruler { position: fixed; background: var(--comty-surface); pointer-events: auto; touch-action: none; }
1099
1160
  .comty-ruler-horizontal { left: 0; top: 0; width: 100vw; height: 1.125rem; border-bottom: .0625rem solid var(--comty-border); cursor: ns-resize; }
1100
1161
  .comty-ruler-vertical { left: 0; top: 0; height: 100vh; width: 1.125rem; border-right: .0625rem solid var(--comty-border); cursor: ew-resize; }
1101
1162
  .comty-ruler-guide { position: fixed; padding: 0; margin: 0; border: 0; background: transparent; color: var(--comty-outline); pointer-events: auto; touch-action: none; }
@@ -1109,14 +1170,43 @@ function getComtyStyle() {
1109
1170
  .comty-ruler-guide span { position: absolute; padding: .125rem .375rem; border-radius: .25rem; color: var(--comty-paper); background: var(--comty-ink); font: .625rem/1.5 ui-monospace, monospace; }
1110
1171
  .comty-ruler-guide-horizontal span { left: 1.5rem; top: .5rem; }
1111
1172
  .comty-ruler-guide-vertical span { top: 1.5rem; left: .5rem; }
1112
- .comty-tool-panel.comty-color-toast { display: flex; align-items: center; gap: .75rem; width: max-content; max-width: calc(100vw - 1rem); padding: .75rem; border: 0; border-radius: .9375rem; overflow: hidden; background: var(--comty-surface); color: var(--comty-foreground); }
1113
- .comty-color-toast[hidden] { display: none; }
1114
- .comty-color-content { display: flex; flex-direction: column; justify-content: center; min-width: 0; }
1115
- .comty-color-token { color: var(--comty-muted); font: .75rem/1.5 ui-monospace, monospace; }
1116
- .comty-color-values { display: flex; align-items: center; gap: .375rem; white-space: nowrap; font-variant-numeric: tabular-nums; }
1117
- .comty-color-rgb, .comty-color-separator, .comty-color-status { color: var(--comty-muted); }
1118
- .comty-color-status { font-size: .75rem; }
1119
- .comty-color-swatch { flex: 0 0 2.375rem; width: 2.375rem; height: 2.375rem; border: 0; border-radius: .375rem; }
1173
+ .comty-property-panel { position: fixed; z-index: 2147483646; width: max-content; max-width: calc(100vw - 1rem); max-height: calc(100vh - 1rem); box-sizing: border-box; margin: 0; padding: .375rem; border: 0; border-radius: 1rem; background: var(--comty-toolbar-group); color: var(--comty-foreground); box-shadow: var(--comty-floating-shadow); font: var(--comty-property-font-size)/1.4 var(--comty-font); pointer-events: auto; overflow-x: hidden; overflow-y: auto; }
1174
+ html[data-comty-property-picking="color"], html[data-comty-property-picking="color"] body, html[data-comty-property-picking="color"] body :not(#comty-root):not(#comty-root *) { cursor: none !important; }
1175
+ html[data-comty-property-picking="font"] body :not(#comty-root):not(#comty-root *) { cursor: url("data:image/svg+xml,${encodeURIComponent(typographyCursor)}") 2 2, default !important; }
1176
+ .comty-property-panel[data-expanded] { width: var(--comty-property-width); }
1177
+ .comty-property-panel[hidden] { display: none; }
1178
+ .comty-color-magnifier { --comty-lens-rim: var(--comty-paper); --comty-lens-line: var(--comty-magnifier-gray); position: fixed; z-index: 2147483647; width: var(--comty-magnifier-size); height: var(--comty-magnifier-size); padding: var(--comty-magnifier-rim); box-sizing: border-box; border-radius: 50%; background: var(--comty-lens-rim); box-shadow: var(--comty-magnifier-shadow); transform: translate(-50%, -50%); pointer-events: none; }
1179
+ .comty-color-magnifier[data-tone="dark"] { --comty-lens-rim: var(--comty-magnifier-dark); --comty-lens-line: var(--comty-paper); }
1180
+ .comty-color-magnifier-glass { position: relative; width: 100%; height: 100%; box-sizing: border-box; overflow: hidden; border: var(--comty-magnifier-line) solid var(--comty-lens-line); border-radius: 50%; }
1181
+ .comty-color-magnifier canvas { display: block; width: 100%; height: 100%; image-rendering: pixelated; }
1182
+ .comty-color-magnifier-glass::after { content: ""; position: absolute; inset: 0; background-image: linear-gradient(to right, var(--comty-lens-line) var(--comty-magnifier-grid-line), transparent var(--comty-magnifier-grid-line)), linear-gradient(to bottom, var(--comty-lens-line) var(--comty-magnifier-grid-line), transparent var(--comty-magnifier-grid-line)); background-size: calc(100% / 9) calc(100% / 9); opacity: .8; }
1183
+ .comty-color-magnifier-glass > span { position: absolute; z-index: 1; left: 50%; top: 50%; width: calc(100% / 9); height: calc(100% / 9); box-sizing: content-box; border: var(--comty-magnifier-line) solid var(--comty-lens-line); border-radius: .125rem; transform: translate(-50%, -50%); }
1184
+ .comty-property-panel button { display: inline-flex; align-items: center; justify-content: center; min-width: 0; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; }
1185
+ .comty-property-panel button:focus-visible { outline: .0625rem solid var(--comty-accent); outline-offset: .0625rem; }
1186
+ .comty-property-detail { margin-bottom: .375rem; }
1187
+ .comty-property-rows { padding: .25rem .625rem .375rem; border-radius: .5rem; background: var(--comty-toolbar-surface); }
1188
+ .comty-property-row { display: grid; grid-template-columns: 3.5rem minmax(0, 1fr); align-items: center; gap: .5rem; min-height: var(--comty-property-row-height); }
1189
+ .comty-property-row > span { color: var(--comty-muted); }
1190
+ .comty-property-row button { justify-content: space-between; gap: .5rem; text-align: left; }
1191
+ .comty-property-row button > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }
1192
+ .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
+ .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; }
1195
+ .comty-property-copy-icon svg { position: absolute; inset: 0; width: 100%; height: 100%; transition: opacity 150ms var(--comty-ease-out), transform 150ms var(--comty-ease-out); }
1196
+ .comty-property-copy-icon svg:last-child { opacity: 0; transform: scale(.5); }
1197
+ .comty-property-row button[data-copied] svg:first-child { opacity: 0; transform: scale(.5); }
1198
+ .comty-property-row button[data-copied] svg:last-child { opacity: 1; transform: scale(1); }
1199
+ @media (prefers-reduced-motion: reduce) { .comty-property-copy-icon svg { transition: none; } }
1200
+ .comty-property-row button[disabled] { cursor: default; opacity: .5; }
1201
+ .comty-property-strip { display: flex; align-items: center; justify-content: center; gap: .375rem; }
1202
+ .comty-property-recent, .comty-property-empty { flex: 0 0 var(--comty-property-swatch); width: var(--comty-property-swatch); height: var(--comty-property-swatch); box-sizing: border-box; border-radius: .5rem; }
1203
+ .comty-property-panel .comty-property-recent { border: .0625rem solid var(--comty-border); font: 600 .875rem/1 var(--comty-font); }
1204
+ .comty-property-panel[data-kind="font"] .comty-property-recent { background: var(--comty-toolbar-surface); }
1205
+ .comty-property-panel .comty-property-recent[aria-pressed="true"] { outline: .0625rem solid var(--comty-muted); outline-offset: .0625rem; }
1206
+ .comty-property-panel .comty-property-recent[aria-pressed="true"]:focus-visible { outline-color: var(--comty-accent); }
1207
+ .comty-property-empty { border: .0625rem dashed var(--comty-border); }
1208
+ .comty-font-target { position: fixed; box-sizing: border-box; border: .0625rem solid var(--comty-accent); pointer-events: none; }
1209
+ .comty-property-feedback { display: block; height: 0; overflow: hidden; }
1120
1210
  .comty-motion-panel { width: 17.5rem; max-width: calc(100vw - 1rem); padding: var(--comty-toolbar-padding); border: 0; border-radius: .9375rem; background: var(--comty-surface); color: var(--comty-foreground); }
1121
1211
  .comty-motion-panel [hidden] { display: none !important; }
1122
1212
  .comty-motion-row { display: flex; align-items: center; gap: .375rem; }
@@ -1154,11 +1244,11 @@ function getComtyStyle() {
1154
1244
  .comty-inspect-margin { --comty-spacing-color: var(--comty-margin-fill); }
1155
1245
  .comty-inspect-padding { --comty-spacing-color: var(--comty-padding-fill); }
1156
1246
  .comty-inspect-gap { --comty-spacing-color: var(--comty-gap-fill); }
1157
- .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-ink); font: 600 .625rem/1.3 ui-monospace, monospace; white-space: nowrap; }
1247
+ .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; }
1158
1248
  .comty-inspector {
1159
1249
  position: fixed; width: 15rem; max-width: calc(100vw - 1rem);
1160
1250
  padding-block: .5rem; border-radius: .5rem;
1161
- color: var(--comty-ink); background: var(--comty-paper);
1251
+ color: var(--comty-foreground); background: var(--comty-surface);
1162
1252
  box-shadow: var(--comty-floating-shadow); font: .625rem/1.5 ui-sans-serif, system-ui, sans-serif;
1163
1253
  pointer-events: none; box-sizing: border-box;
1164
1254
  }
@@ -1173,6 +1263,8 @@ function getComtyStyle() {
1173
1263
  .comty-inspector button:focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .125rem; }
1174
1264
  .comty-inspector-value { display: flex; flex-direction: column; align-items: flex-end; min-width: 0; }
1175
1265
  .comty-inspector-value > button, .comty-inspector-tokens button { max-width: 100%; }
1266
+ .comty-inspector-picker { display: inline-flex; align-items: center; gap: .25rem; }
1267
+ .comty-inspector-swatch { display: inline-block; flex: 0 0 .625rem; width: .625rem; height: .625rem; border: .0625rem solid var(--comty-border); border-radius: .1875rem; }
1176
1268
  .comty-inspector-tokens { display: flex; flex-direction: column; align-items: flex-end; max-width: 100%; }
1177
1269
  .comty-inspector-tokens button { font-family: ui-monospace, monospace; color: var(--comty-muted); }
1178
1270
  .comty-inspector-details { max-height: 50vh; overflow-y: auto; }
@@ -1183,8 +1275,8 @@ function getComtyStyle() {
1183
1275
  .comty-create-bubble {
1184
1276
  position: fixed;
1185
1277
  z-index: 2147483647;
1186
- background: white;
1187
- border: 1px solid rgba(0,0,0,0.05);
1278
+ background: var(--comty-surface);
1279
+ border: 0;
1188
1280
  box-shadow: 0 4px 20px rgba(0,0,0,.1);
1189
1281
  border-radius: 999px;
1190
1282
  width: 320px;
@@ -1218,13 +1310,13 @@ function getComtyStyle() {
1218
1310
  outline: none;
1219
1311
  background: transparent;
1220
1312
  overflow-y: auto;
1221
- color: #111827;
1313
+ color: var(--comty-foreground);
1222
1314
  scrollbar-width: thin;
1223
1315
  scrollbar-color: gray transparent;
1224
1316
  }
1225
1317
 
1226
1318
  .comty-create-textarea::placeholder {
1227
- color: #9ca3af;
1319
+ color: var(--comty-muted);
1228
1320
  }
1229
1321
 
1230
1322
  .comty-create-actions {
@@ -1241,7 +1333,7 @@ function getComtyStyle() {
1241
1333
  height: 32px;
1242
1334
  padding: 0;
1243
1335
  border-radius: 50%;
1244
- background: #e5e7eb;
1336
+ background: var(--comty-border);
1245
1337
  color: white;
1246
1338
  border: none;
1247
1339
  cursor: pointer;
@@ -2097,7 +2189,7 @@ function createScreenshotCapture() {
2097
2189
 
2098
2190
  const clonedBody = clone.querySelector("body");
2099
2191
  if (clonedBody instanceof HTMLElement) {
2100
- clonedBody.style.width = `${captureWidth}px`;
2192
+ clonedBody.style.width = getComputedStyle(document.body).width;
2101
2193
  clonedBody.style.minHeight = `${documentHeight}px`;
2102
2194
  clonedBody.style.margin = clonedBody.style.margin || "0";
2103
2195
  }
@@ -2128,6 +2220,109 @@ function createScreenshotCapture() {
2128
2220
  return { waitForPaint, waitForCustomElements, captureViewportScreenshot };
2129
2221
  }
2130
2222
 
2223
+ // Page-local counterpart of the native eyedropper; never captures outside the page.
2224
+ function createColorMagnifier({ root, isComtyElement, capture }) {
2225
+ const lens = document.createElement('div');
2226
+ lens.className = 'comty-color-magnifier';
2227
+ lens.setAttribute('aria-hidden', 'true');
2228
+ lens.hidden = true;
2229
+ lens.innerHTML = '<div class="comty-color-magnifier-glass"><canvas width="9" height="9"></canvas><span></span></div>';
2230
+ root.appendChild(lens);
2231
+ const canvas = lens.querySelector('canvas');
2232
+ const context = canvas.getContext('2d', { willReadFrequently: true });
2233
+ let enabled = false, image = null, point = null, revision = 0, timer = 0;
2234
+ let sampledHex = null;
2235
+
2236
+ function paint() {
2237
+ if (!enabled || !point || !context) return;
2238
+ const { x, y, element } = point;
2239
+ if (!element.isConnected) { lens.hidden = true; return; }
2240
+ // An unavailable/unsafe page capture falls back to the element's CSS colour.
2241
+ let background = element;
2242
+ while (background.parentElement && getComputedStyle(background).backgroundColor === 'rgba(0, 0, 0, 0)') background = background.parentElement;
2243
+ const color = getComputedStyle(background).backgroundColor;
2244
+ context.clearRect(0, 0, 9, 9);
2245
+ context.fillStyle = getComputedStyle(root).getPropertyValue('--comty-paper');
2246
+ context.fillRect(0, 0, 9, 9);
2247
+ context.fillStyle = color;
2248
+ context.fillRect(0, 0, 9, 9);
2249
+ if (image) context.drawImage(image, Math.floor(x) - 4, Math.floor(y) - 4, 9, 9, 0, 0, 9, 9);
2250
+ lens.dataset.preview = image ? 'pixels' : 'color';
2251
+ sampledHex = null;
2252
+ try {
2253
+ const [r, g, b] = context.getImageData(4, 4, 1, 1).data;
2254
+ if (image) sampledHex = '#' + [r, g, b].map(channel => channel.toString(16).padStart(2, '0')).join('');
2255
+ lens.dataset.tone = .2126 * r + .7152 * g + .0722 * b < 128 ? 'dark' : 'light';
2256
+ } catch { lens.dataset.tone = 'light'; }
2257
+ lens.style.left = `${x}px`;
2258
+ lens.style.top = `${y}px`;
2259
+ lens.hidden = false;
2260
+ }
2261
+
2262
+ async function refresh() {
2263
+ const version = ++revision;
2264
+ image = null;
2265
+ sampledHex = null;
2266
+ if (!enabled) return;
2267
+ try {
2268
+ const { blob } = await capture();
2269
+ if (!enabled || version !== revision) return;
2270
+ const dataUrl = await new Promise((resolve, reject) => {
2271
+ const reader = new FileReader();
2272
+ reader.onload = () => resolve(reader.result);
2273
+ reader.onerror = reject;
2274
+ reader.readAsDataURL(blob);
2275
+ });
2276
+ const next = new Image();
2277
+ next.src = dataUrl;
2278
+ await next.decode();
2279
+ if (!enabled || version !== revision) return;
2280
+ // Verify the bitmap can be sampled before using it for both preview and pick.
2281
+ const probe = document.createElement('canvas').getContext('2d');
2282
+ probe.drawImage(next, 0, 0);
2283
+ probe.getImageData(0, 0, 1, 1);
2284
+ image = next;
2285
+ paint();
2286
+ } catch { /* The CSS-colour fallback remains available without screen permission. */ }
2287
+ }
2288
+
2289
+ function setEnabled(value) {
2290
+ if (enabled === value) return;
2291
+ enabled = value;
2292
+ lens.hidden = true;
2293
+ point = null;
2294
+ clearTimeout(timer);
2295
+ if (enabled) refresh();
2296
+ else { revision++; image = null; sampledHex = null; }
2297
+ }
2298
+
2299
+ document.addEventListener('pointermove', event => {
2300
+ if (!enabled) return;
2301
+ if (!(event.target instanceof Element) || isComtyElement(event.target)) {
2302
+ point = null;
2303
+ sampledHex = null;
2304
+ lens.hidden = true;
2305
+ return;
2306
+ }
2307
+ point = { x: event.clientX, y: event.clientY, element: event.target };
2308
+ paint();
2309
+ }, true);
2310
+ document.documentElement.addEventListener('pointerleave', () => { point = null; lens.hidden = true; });
2311
+ function invalidate(event) {
2312
+ if (!enabled || event.target instanceof Element && isComtyElement(event.target)) return;
2313
+ revision++;
2314
+ image = null;
2315
+ sampledHex = null;
2316
+ point = null;
2317
+ lens.hidden = true;
2318
+ clearTimeout(timer);
2319
+ timer = setTimeout(refresh, 120);
2320
+ }
2321
+ document.addEventListener('scroll', invalidate, true);
2322
+ window.addEventListener('resize', invalidate);
2323
+ return { setEnabled, sample: () => sampledHex };
2324
+ }
2325
+
2131
2326
  // Comment thread UI and actions. The browser shell supplies the shared comment state.
2132
2327
  function createCommentThread(context) {
2133
2328
  const {
@@ -2631,6 +2826,216 @@ function createCommentThread(context) {
2631
2826
  }
2632
2827
  }
2633
2828
 
2829
+ const edgeAxis = { top: "y", bottom: "y", left: "x", right: "x" };
2830
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
2831
+
2832
+ /** Pick a nearby border segment, including when the pointer is just outside it. */
2833
+ function nearestMeasurementEdge(candidates, point, { axis, exclude, tolerance = 10 } = {}) {
2834
+ let nearest = null;
2835
+ let distance = tolerance;
2836
+ for (const { element, rect } of candidates) {
2837
+ if (rect.width <= 0 || rect.height <= 0) continue;
2838
+ for (const edge of ["top", "bottom", "left", "right"]) {
2839
+ if (axis && edgeAxis[edge] !== axis) continue;
2840
+ if (exclude?.element === element && exclude.edge === edge) continue;
2841
+ const vertical = edgeAxis[edge] === "y";
2842
+ const x = vertical ? clamp(point.x, rect.left, rect.right) : rect[edge];
2843
+ const y = vertical ? rect[edge] : clamp(point.y, rect.top, rect.bottom);
2844
+ const nextDistance = Math.hypot(point.x - x, point.y - y);
2845
+ if (nextDistance > distance || (nearest && nextDistance === distance)) continue;
2846
+ distance = nextDistance;
2847
+ nearest = { element, edge, axis: edgeAxis[edge], offset: vertical ? (x - rect.left) / rect.width : (y - rect.top) / rect.height };
2848
+ }
2849
+ }
2850
+ return nearest;
2851
+ }
2852
+
2853
+ function measurementPoint(anchor, rect, scroll = { x: 0, y: 0 }) {
2854
+ if (!anchor.element) return { x: anchor.x - scroll.x, y: anchor.y - scroll.y };
2855
+ return anchor.axis === "y"
2856
+ ? { x: rect.left + rect.width * anchor.offset, y: rect[anchor.edge] }
2857
+ : { x: rect[anchor.edge], y: rect.top + rect.height * anchor.offset };
2858
+ }
2859
+
2860
+ /** Border measurements live for one activation of the existing ruler tool. */
2861
+ function createMeasurements({ root, isComtyElement, isElementTreeHidden, onExit }) {
2862
+ let enabled = false;
2863
+ let hover = null;
2864
+ let drag = null;
2865
+ let pointer = null;
2866
+ let signature = "";
2867
+ const measurements = [];
2868
+ const svgNS = "http://www.w3.org/2000/svg";
2869
+ const layer = document.createElementNS(svgNS, "svg");
2870
+ layer.classList.add("comty-distance-layer");
2871
+ layer.setAttribute("aria-hidden", "true");
2872
+ root.prepend(layer);
2873
+ const announcement = document.createElement("span");
2874
+ announcement.className = "comty-distance-announcement";
2875
+ announcement.setAttribute("role", "status");
2876
+ root.appendChild(announcement);
2877
+
2878
+ function rectOf(element) {
2879
+ if (element === document.documentElement) {
2880
+ const width = Math.max(element.clientWidth, element.scrollWidth);
2881
+ const height = Math.max(element.clientHeight, element.scrollHeight);
2882
+ return { left: -scrollX, top: -scrollY, right: width - scrollX, bottom: height - scrollY, width, height };
2883
+ }
2884
+ return element.getBoundingClientRect();
2885
+ }
2886
+ function pointOf(anchor) {
2887
+ return measurementPoint(anchor, anchor.element && rectOf(anchor.element), { x: scrollX, y: scrollY });
2888
+ }
2889
+ function candidatesAt(point) {
2890
+ const elements = new Set();
2891
+ // Sample around the cursor as well as underneath it to catch an outside edge.
2892
+ for (const [dx, dy] of [[0, 0], [-10, 0], [10, 0], [0, -10], [0, 10]]) {
2893
+ for (const hit of document.elementsFromPoint(clamp(point.x + dx, 0, innerWidth - 1), clamp(point.y + dy, 0, innerHeight - 1))) {
2894
+ if (isComtyElement(hit)) continue;
2895
+ for (let element = hit; element; element = element.parentElement) elements.add(element);
2896
+ }
2897
+ }
2898
+ return [...elements].filter(element => !isElementTreeHidden(element)).map(element => ({ element, rect: rectOf(element) }));
2899
+ }
2900
+ function findEdge(point, start) {
2901
+ return nearestMeasurementEdge(candidatesAt(point), point, { axis: start?.axis, exclude: start });
2902
+ }
2903
+ function setCursor(axis) {
2904
+ document.documentElement.classList.toggle("comty-measure-vertical", axis === "y");
2905
+ document.documentElement.classList.toggle("comty-measure-horizontal", axis === "x");
2906
+ }
2907
+ function updatePointer(event) {
2908
+ pointer = { x: event.clientX, y: event.clientY };
2909
+ hover = findEdge(pointer, drag?.start);
2910
+ if (drag) {
2911
+ drag.end = hover || { x: pointer.x + scrollX, y: pointer.y + scrollY };
2912
+ // Keep the two anchors perpendicular when both borders share that coordinate.
2913
+ if (hover) {
2914
+ const start = pointOf(drag.start), rect = rectOf(hover.element);
2915
+ drag.end = { ...hover, offset: drag.start.axis === "y"
2916
+ ? clamp((start.x - rect.left) / rect.width, 0, 1)
2917
+ : clamp((start.y - rect.top) / rect.height, 0, 1) };
2918
+ }
2919
+ }
2920
+ setCursor(drag?.start.axis || hover?.axis);
2921
+ render();
2922
+ }
2923
+ function node(tag, attributes, parent = layer) {
2924
+ const element = document.createElementNS(svgNS, tag);
2925
+ for (const [key, value] of Object.entries(attributes)) element.setAttribute(key, value);
2926
+ parent.appendChild(element);
2927
+ return element;
2928
+ }
2929
+ function valid(anchor) {
2930
+ return !anchor.element || (anchor.element.isConnected && !isElementTreeHidden(anchor.element));
2931
+ }
2932
+ function render() {
2933
+ if (!enabled) return;
2934
+ const visible = [...measurements, ...(drag ? [drag] : [])].filter(item => valid(item.start) && valid(item.end));
2935
+ const geometry = visible.map(item => ({ start: pointOf(item.start), end: pointOf(item.end), axis: item.start.axis }));
2936
+ const hoverPoint = hover && valid(hover) ? pointOf(hover) : null;
2937
+ const next = JSON.stringify([geometry, hoverPoint]);
2938
+ if (signature === next) return;
2939
+ signature = next;
2940
+ layer.replaceChildren();
2941
+ for (const { start, end, axis } of geometry) {
2942
+ const vertical = axis === "y";
2943
+ const distance = Math.round(Math.abs(end[axis] - start[axis]));
2944
+ if (distance < 1) continue;
2945
+ const x2 = vertical ? start.x : end.x, y2 = vertical ? end.y : start.y;
2946
+ const group = node("g", { "data-comty-distance": distance });
2947
+ node("path", { d: `M${start.x} ${start.y}L${x2} ${y2}`, class: "comty-distance-line" }, group);
2948
+ if (Math.hypot(end.x - x2, end.y - y2) > 1) node("path", { d: `M${x2} ${y2}L${end.x} ${end.y}`, class: "comty-distance-extension" }, group);
2949
+ const direction = Math.sign(end[axis] - start[axis]);
2950
+ for (const [x, y, inward] of [[start.x, start.y, direction], [x2, y2, -direction]]) {
2951
+ const points = vertical ? `${x},${y} ${x - 3},${y + inward * 6} ${x + 3},${y + inward * 6}` : `${x},${y} ${x + inward * 6},${y - 3} ${x + inward * 6},${y + 3}`;
2952
+ node("polygon", { points, class: "comty-distance-arrow" }, group);
2953
+ }
2954
+ const text = String(distance), width = text.length * 7.5 + 12;
2955
+ const labelX = vertical ? start.x - width - 8 : (start.x + x2 - width) / 2;
2956
+ const labelY = vertical ? (start.y + y2) / 2 - 10 : start.y - 28;
2957
+ const label = node("g", { transform: `translate(${clamp(labelX, 2, innerWidth - width - 2)} ${clamp(labelY, 2, innerHeight - 22)})` }, group);
2958
+ node("rect", { width, height: 20, rx: 3, class: "comty-distance-label" }, label);
2959
+ node("text", { x: width / 2, y: 14, "text-anchor": "middle", class: "comty-distance-value" }, label).textContent = text;
2960
+ }
2961
+ if (hoverPoint) node("circle", { cx: hoverPoint.x, cy: hoverPoint.y, r: 4, class: "comty-distance-snap" });
2962
+ }
2963
+ function cancel() {
2964
+ const pointerId = drag?.pointerId;
2965
+ drag = null;
2966
+ if (pointerId !== undefined && document.documentElement.hasPointerCapture(pointerId)) document.documentElement.releasePointerCapture(pointerId);
2967
+ hover = null;
2968
+ pointer = null;
2969
+ setCursor(null);
2970
+ render();
2971
+ }
2972
+ function setEnabled(value) {
2973
+ if (enabled === value) return;
2974
+ enabled = value;
2975
+ layer.style.display = value ? "" : "none";
2976
+ if (!value) {
2977
+ measurements.length = 0;
2978
+ cancel();
2979
+ layer.replaceChildren();
2980
+ announcement.textContent = "";
2981
+ signature = "";
2982
+ }
2983
+ }
2984
+ function isHostEvent(event) {
2985
+ if (!enabled || !(event.target instanceof Element)) return false;
2986
+ if (!isComtyElement(event.target)) return true;
2987
+ // The outer ruler strip must still let us anchor to the page's top/left edge.
2988
+ return event.target.matches(".comty-ruler") && (event.clientX + scrollX <= 10 || event.clientY + scrollY <= 10);
2989
+ }
2990
+ document.addEventListener("pointermove", event => {
2991
+ if (!enabled) return;
2992
+ if (drag && event.pointerId !== drag.pointerId) return;
2993
+ if (!isHostEvent(event) && !drag) { hover = null; setCursor(null); render(); return; }
2994
+ event.stopImmediatePropagation();
2995
+ updatePointer(event);
2996
+ }, true);
2997
+ document.addEventListener("pointerdown", event => {
2998
+ if (!isHostEvent(event) || event.button !== 0) return;
2999
+ event.preventDefault(); event.stopImmediatePropagation();
3000
+ updatePointer(event);
3001
+ if (hover) {
3002
+ drag = { start: hover, end: hover, pointerId: event.pointerId };
3003
+ if (event.isTrusted) document.documentElement.setPointerCapture(event.pointerId);
3004
+ }
3005
+ }, true);
3006
+ document.addEventListener("pointerup", event => {
3007
+ if (!drag || event.pointerId !== drag.pointerId) return;
3008
+ event.preventDefault(); event.stopImmediatePropagation();
3009
+ updatePointer(event);
3010
+ const distance = Math.round(Math.abs(pointOf(drag.end)[drag.start.axis] - pointOf(drag.start)[drag.start.axis]));
3011
+ if (distance > 0) {
3012
+ measurements.push(drag);
3013
+ announcement.textContent = `${distance} pixels ${drag.start.axis === "y" ? "vertically" : "horizontally"}`;
3014
+ }
3015
+ drag = null;
3016
+ if (document.documentElement.hasPointerCapture(event.pointerId)) document.documentElement.releasePointerCapture(event.pointerId);
3017
+ render();
3018
+ }, true);
3019
+ document.addEventListener("click", event => {
3020
+ if (isHostEvent(event)) { event.preventDefault(); event.stopImmediatePropagation(); }
3021
+ }, true);
3022
+ document.addEventListener("keydown", event => {
3023
+ if (!enabled || event.key !== "Escape" || event.target.closest?.(".comty-auth-dialog, input, textarea, [contenteditable=true]")) return;
3024
+ event.preventDefault(); event.stopImmediatePropagation();
3025
+ if (drag) cancel(); else onExit();
3026
+ }, true);
3027
+ document.addEventListener("pointercancel", cancel, true);
3028
+ document.documentElement.addEventListener("lostpointercapture", event => { if (drag?.pointerId === event.pointerId) cancel(); });
3029
+ document.documentElement.addEventListener("pointerleave", () => { if (!drag) cancel(); });
3030
+ window.addEventListener("blur", cancel);
3031
+ document.addEventListener("scroll", () => {
3032
+ if (!enabled) return;
3033
+ if (pointer) updatePointer({ clientX: pointer.x, clientY: pointer.y });
3034
+ else render();
3035
+ }, { capture: true, passive: true });
3036
+ return { setEnabled, render };
3037
+ }
3038
+
2634
3039
  // Shared browser runtime. Both npm and the standalone CDN script use this file.
2635
3040
  const instanceKey = Symbol.for("comty.sdk.instance");
2636
3041
 
@@ -2768,6 +3173,13 @@ function initComty(options = {}) {
2768
3173
  const style = document.createElement("style");
2769
3174
  style.innerHTML = getComtyStyle();
2770
3175
  document.head.appendChild(style);
3176
+ if (!document.querySelector('link[data-comty-font]')) {
3177
+ const font = document.createElement('link');
3178
+ font.rel = 'stylesheet';
3179
+ font.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap';
3180
+ font.dataset.comtyFont = '';
3181
+ document.head.appendChild(font);
3182
+ }
2771
3183
 
2772
3184
  const toolbar = document.createElement("div");
2773
3185
  toolbar.className = "comty-toolbar";
@@ -2781,6 +3193,12 @@ function initComty(options = {}) {
2781
3193
  toolbarSurface.className = "comty-toolbar-surface";
2782
3194
  toolbar.appendChild(toolbarSurface);
2783
3195
  const icon = (paths) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
3196
+ const dragHandle = document.createElement("span");
3197
+ dragHandle.className = "comty-toolbar-grip";
3198
+ dragHandle.title = "Drag to move";
3199
+ dragHandle.setAttribute("aria-hidden", "true");
3200
+ dragHandle.innerHTML = icon('<g fill="currentColor" stroke="none"><circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/></g>');
3201
+ toolbar.appendChild(dragHandle);
2784
3202
  function toolButton(label, paths, action) {
2785
3203
  const button = document.createElement("button");
2786
3204
  button.className = "comty-control-button";
@@ -2828,6 +3246,10 @@ function initComty(options = {}) {
2828
3246
  agentHistoryBtn.setAttribute("aria-expanded", "false");
2829
3247
  agentTools.appendChild(agentHistoryBtn);
2830
3248
  }
3249
+ const primaryTools = document.createElement("div");
3250
+ primaryTools.className = "comty-tool-primary";
3251
+ toolbar.insertBefore(primaryTools, browseBtn);
3252
+ for (const button of [browseBtn, inspectorBtn, btn, agentBtn].filter(Boolean)) primaryTools.appendChild(button);
2831
3253
  const inspectorTools = document.createElement("div");
2832
3254
  inspectorTools.className = "comty-tool-context";
2833
3255
  inspectorTools.hidden = true;
@@ -2841,11 +3263,24 @@ function initComty(options = {}) {
2841
3263
  let rulerDrag = null;
2842
3264
  const rulerGuides = [];
2843
3265
  let colorPickerController = null;
2844
- let colorToastTimer = null;
2845
- let colorToastAnimation = null;
3266
+ const recentColors = [];
3267
+ const recentFonts = [];
3268
+ let propertyKind = null;
3269
+ let propertySelection = null;
3270
+ let propertyOpener = null;
3271
+ let propertyAnimation = null;
3272
+ let propertyStripAnimation = null;
3273
+ let propertyPicking = null;
3274
+ const colorMagnifier = createColorMagnifier({ root, isComtyElement, capture: () => captureViewportScreenshot() });
2846
3275
  const overlayTools = document.createElement("div");
2847
3276
  overlayTools.className = "comty-overlay-tools";
2848
3277
  root.prepend(overlayTools);
3278
+ const measurements = createMeasurements({
3279
+ root,
3280
+ isComtyElement,
3281
+ isElementTreeHidden,
3282
+ onExit() { rulersEnabled = false; syncInspectorTools(); },
3283
+ });
2849
3284
  const gridLayer = document.createElement("div");
2850
3285
  gridLayer.className = "comty-layout-guides";
2851
3286
  overlayTools.appendChild(gridLayer);
@@ -2857,6 +3292,11 @@ function initComty(options = {}) {
2857
3292
  toolPanel.className = "comty-tool-panel";
2858
3293
  toolPanel.hidden = true;
2859
3294
  root.appendChild(toolPanel);
3295
+ const propertyPanel = document.createElement("section");
3296
+ propertyPanel.className = "comty-property-panel";
3297
+ propertyPanel.id = "comty-property-panel";
3298
+ propertyPanel.hidden = true;
3299
+ root.appendChild(propertyPanel);
2860
3300
  const xrayStyle = document.createElement("style");
2861
3301
  // Use an outline overlay: geometry and the page's existing styles stay intact.
2862
3302
  xrayStyle.textContent = `body *:not(#comty-root):not(#comty-root *) { outline: 1px solid ${getComputedStyle(root).getPropertyValue('--comty-xray').trim()} !important; }`;
@@ -2865,10 +3305,17 @@ function initComty(options = {}) {
2865
3305
  syncInspectorTools();
2866
3306
  });
2867
3307
  const rulersBtn = toolButton("Rulers · R", '<path d="M3 3h18v6H9v12H3Z"/><path d="M7 3v3m4-3v3m4-3v3M3 11h3m-3 4h3m-3 4h3"/>', () => {
3308
+ closePropertyPanel();
3309
+ if (motion.enabled) setMotionEnabled(false);
2868
3310
  rulersEnabled = !rulersEnabled;
3311
+ selectedElement = null;
3312
+ measuredRegion = null;
3313
+ inspectDrag = null;
3314
+ hideInspector();
2869
3315
  syncInspectorTools();
2870
3316
  });
2871
3317
  const gridBtn = toolButton("Layout guides · G", '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 10h18M10 3v18"/>', () => {
3318
+ closePropertyPanel();
2872
3319
  if (motion.enabled) setMotionEnabled(false);
2873
3320
  gridPanelOpen = !gridPanelOpen;
2874
3321
  if (gridPanelOpen && !layoutGuides.length) addLayoutGuide();
@@ -2876,10 +3323,16 @@ function initComty(options = {}) {
2876
3323
  syncInspectorTools();
2877
3324
  });
2878
3325
  gridBtn.setAttribute("aria-expanded", "false");
2879
- const pipetteBtn = toolButton("Sample color · P", '<path d="m16 3 5 5-4 4-5-5Zm-3 5L4 17v3h3l9-9M11 6l7 7"/>', sampleColor);
3326
+ const pipetteBtn = toolButton("Sample color · P", '<path d="m16 3 5 5-4 4-5-5Zm-3 5L4 17v3h3l9-9M11 6l7 7"/>', event => togglePropertyPanel("color", pipetteBtn, event.shiftKey));
3327
+ pipetteBtn.title = "Sample color · P · Shift-click to sample the screen";
3328
+ const typographyBtn = toolButton("Typography · T", '<path d="m5 20 7-16 7 16M8 14h8M3 20h5m8 0h5M10 4h4"/>', () => togglePropertyPanel("font", typographyBtn));
3329
+ for (const button of [pipetteBtn, typographyBtn]) {
3330
+ button.setAttribute("aria-controls", propertyPanel.id);
3331
+ button.setAttribute("aria-expanded", "false");
3332
+ }
2880
3333
  const motionBtn = toolButton("Motion · M", '<circle cx="12" cy="14" r="8"/><path d="M9 2h6m-3 4V2m6 5 2-2m-8 5v4l3 2"/>', () => setMotionEnabled(!motion.enabled));
2881
3334
  motionBtn.setAttribute("aria-expanded", "false");
2882
- for (const button of [xrayBtn, rulersBtn, gridBtn, pipetteBtn, motionBtn]) inspectorTools.appendChild(button);
3335
+ for (const button of [xrayBtn, rulersBtn, gridBtn, pipetteBtn, typographyBtn, motionBtn]) inspectorTools.appendChild(button);
2883
3336
 
2884
3337
  const motion = { enabled: false, speed: 1, paused: false, managed: new Map(), shadowRoots: [], nextRootScan: 0 };
2885
3338
  const motionPanel = document.createElement('section');
@@ -2928,6 +3381,9 @@ function initComty(options = {}) {
2928
3381
  motion.enabled = enabled; motionPanel.hidden = !enabled;
2929
3382
  motionBtn.setAttribute('aria-pressed', String(enabled)); motionBtn.setAttribute('aria-expanded', String(enabled));
2930
3383
  if (enabled) {
3384
+ closePropertyPanel();
3385
+ rulersEnabled = false;
3386
+ syncInspectorTools();
2931
3387
  gridPanelOpen = false; toolPanel.hidden = true; gridBtn.setAttribute('aria-expanded', 'false');
2932
3388
  measuredRegion = null; inspectDrag = null; selectedElement = null; hideInspector();
2933
3389
  motion.nextRootScan = 0; updateMotion(); renderMotionPanel(); positionToolbar(); ensureLiveTracking();
@@ -3031,32 +3487,101 @@ function initComty(options = {}) {
3031
3487
  }
3032
3488
  }
3033
3489
 
3490
+ let closeGuideMenu = () => {};
3491
+ function guideSelect(label, options, value, onChange) {
3492
+ const trigger = document.createElement("button");
3493
+ trigger.type = "button";
3494
+ trigger.className = "comty-guide-select";
3495
+ trigger.setAttribute("aria-label", label);
3496
+ trigger.setAttribute("aria-haspopup", "menu");
3497
+ trigger.setAttribute("aria-expanded", "false");
3498
+ function setValue(next) {
3499
+ trigger.value = next;
3500
+ trigger.innerHTML = `${options.find(option => option[0] === next)[1]}${icon('<path d="m8 10 4 4 4-4"/>')}`;
3501
+ }
3502
+ setValue(value);
3503
+ function open() {
3504
+ const wasOpen = trigger.getAttribute("aria-expanded") === "true";
3505
+ closeGuideMenu();
3506
+ if (wasOpen) return;
3507
+ root.querySelectorAll('.comty-popover').forEach(popover => popover.remove());
3508
+ const menu = document.createElement("div");
3509
+ menu.className = "comty-popover comty-guide-popover";
3510
+ menu.setAttribute("role", "menu");
3511
+ menu.setAttribute("aria-label", label);
3512
+ const controller = new AbortController();
3513
+ const close = (focus = false) => {
3514
+ controller.abort(); menu.remove();
3515
+ trigger.setAttribute("aria-expanded", "false");
3516
+ if (focus && trigger.isConnected) trigger.focus();
3517
+ closeGuideMenu = () => {};
3518
+ };
3519
+ closeGuideMenu = close;
3520
+ const items = options.map(([key, name]) => {
3521
+ const item = document.createElement("button");
3522
+ item.type = "button";
3523
+ item.className = "comty-popover-item";
3524
+ item.setAttribute("role", "menuitemradio");
3525
+ item.setAttribute("aria-checked", String(key === trigger.value));
3526
+ item.textContent = name;
3527
+ if (key === trigger.value) {
3528
+ const check = document.createElement("span");
3529
+ check.className = "comty-popover-item-check";
3530
+ check.innerHTML = icon('<path d="m5 12 4 4 10-10"/>');
3531
+ item.appendChild(check);
3532
+ }
3533
+ item.onclick = () => { close(); setValue(key); onChange(key); if (trigger.isConnected) trigger.focus(); };
3534
+ menu.appendChild(item);
3535
+ return item;
3536
+ });
3537
+ toolPanel.appendChild(menu);
3538
+ trigger.setAttribute("aria-expanded", "true");
3539
+ const rect = trigger.getBoundingClientRect();
3540
+ menu.style.left = `${Math.max(8, Math.min(rect.right - menu.offsetWidth, innerWidth - menu.offsetWidth - 8))}px`;
3541
+ menu.style.top = `${rect.bottom + menu.offsetHeight + 4 <= innerHeight - 8 ? rect.bottom + 4 : Math.max(8, rect.top - menu.offsetHeight - 4)}px`;
3542
+ items[options.findIndex(option => option[0] === trigger.value)].focus();
3543
+ menu.onkeydown = event => {
3544
+ const index = items.indexOf(document.activeElement);
3545
+ if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
3546
+ event.preventDefault(); event.stopPropagation();
3547
+ items[event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + items.length) % items.length].focus();
3548
+ } else if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(true); }
3549
+ else if (event.key === "Tab") close();
3550
+ };
3551
+ document.addEventListener("pointerdown", event => {
3552
+ if (!menu.contains(event.target) && !trigger.contains(event.target)) close();
3553
+ }, {capture: true, signal: controller.signal});
3554
+ window.addEventListener("resize", () => close(), {signal: controller.signal});
3555
+ }
3556
+ trigger.onclick = open;
3557
+ trigger.onkeydown = event => {
3558
+ if (["ArrowDown", "ArrowUp"].includes(event.key)) { event.preventDefault(); open(); }
3559
+ };
3560
+ return { trigger, setValue };
3561
+ }
3562
+
3034
3563
  function renderGridPanel() {
3035
- clearTimeout(colorToastTimer);
3036
- colorToastAnimation?.cancel();
3037
- colorToastAnimation = null;
3038
- toolPanel.classList.remove("comty-color-toast");
3564
+ closeGuideMenu();
3039
3565
  toolPanel.hidden = !gridPanelOpen || !session.inspect;
3040
3566
  gridBtn.setAttribute("aria-expanded", String(gridPanelOpen && session.inspect));
3041
3567
  if (toolPanel.hidden) return;
3042
3568
  toolPanel.setAttribute("aria-label", "Layout guides");
3043
- toolPanel.innerHTML = `<div class="comty-tool-panel-header"><strong>Layout guides</strong><select aria-label="Guide type"><option value="columns">Columns</option><option value="rows">Rows</option><option value="grid">Grid</option></select><button type="button" data-add aria-label="Add layout guide">${icon('<path d="M12 5v14M5 12h14"/>')}</button></div><div class="comty-guide-list"></div>`;
3569
+ toolPanel.innerHTML = `<div class="comty-tool-panel-header"><strong>Layout guides</strong><span data-guide-type></span><button type="button" data-add aria-label="Add layout guide">${icon('<path d="M12 5v14M5 12h14"/>')}</button></div><div class="comty-guide-list"></div>`;
3044
3570
  const selectedGuide = layoutGuides.find(guide => guide.id === selectedGuideId) || layoutGuides[0];
3045
3571
  selectedGuideId = selectedGuide?.id ?? null;
3046
- const typeSelect = toolPanel.querySelector("select");
3047
- typeSelect.value = selectedGuide?.kind ?? "columns";
3048
- typeSelect.onchange = () => {
3572
+ const typeSelect = guideSelect("Guide type", [["columns", "Columns"], ["rows", "Rows"], ["grid", "Grid"]], selectedGuide?.kind ?? "columns", value => {
3049
3573
  const guide = layoutGuides.find(guide => guide.id === selectedGuideId);
3050
3574
  if (guide) {
3051
- if ((guide.kind === "grid") !== (typeSelect.value === "grid")) guide.size = typeSelect.value === "grid" ? 8 : 72;
3052
- guide.kind = typeSelect.value;
3053
- } else addLayoutGuide(typeSelect.value);
3575
+ if ((guide.kind === "grid") !== (value === "grid")) guide.size = value === "grid" ? 8 : 72;
3576
+ guide.kind = value;
3577
+ } else addLayoutGuide(value);
3054
3578
  renderGridPanel();
3055
3579
  syncInspectorTools();
3056
- toolPanel.querySelector("select").focus();
3057
- };
3580
+ toolPanel.querySelector('[aria-label="Guide type"]').focus();
3581
+ });
3582
+ toolPanel.querySelector("[data-guide-type]").replaceWith(typeSelect.trigger);
3058
3583
  toolPanel.querySelector("[data-add]").onclick = () => {
3059
- addLayoutGuide(toolPanel.querySelector("select").value);
3584
+ addLayoutGuide(typeSelect.trigger.value);
3060
3585
  renderGridPanel();
3061
3586
  };
3062
3587
  const list = toolPanel.querySelector(".comty-guide-list");
@@ -3069,8 +3594,14 @@ function initComty(options = {}) {
3069
3594
  item.innerHTML = `<div class="comty-guide-row"><button type="button" data-edit aria-expanded="false">${escapeInspectorText(name)} <span aria-hidden="true">›</span></button><button type="button" data-visible aria-label="Show ${guide.kind}" aria-pressed="${guide.visible}">${icon('<path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/>')}</button><button type="button" data-remove aria-label="Remove ${guide.kind}">${icon('<path d="M5 12h14"/>')}</button></div><div class="comty-guide-fields" hidden></div>`;
3070
3595
  const fields = item.querySelector(".comty-guide-fields");
3071
3596
  const numberField = (label, key, min, max) => `<label>${label}<input type="number" data-field="${key}" value="${guide[key]}" min="${min}" max="${max}" step="1"></label>`;
3072
- fields.innerHTML = `${guide.kind !== "grid" ? numberField("Count", "count", 1, 24) : ""}<label>Color<input type="color" data-field="color" value="${guide.color}"></label>${numberField("Opacity %", "opacity", 0, 100)}${guide.kind !== "grid" ? `<label>Type<select data-field="align"><option value="stretch">Stretch</option><option value="start">Start</option><option value="center">Center</option><option value="end">End</option></select></label>` : ""}${numberField(guide.kind === "grid" ? "Size" : guide.kind === "rows" ? "Height" : "Width", "size", 1, 4096)}${numberField("Offset", "offset", 0, 4096)}${guide.kind !== "grid" ? numberField("Gutter", "gutter", 0, 800) : ""}`;
3073
- if (guide.kind !== "grid") fields.querySelector("select").value = guide.align;
3597
+ fields.innerHTML = `${guide.kind !== "grid" ? numberField("Count", "count", 1, 24) : ""}<label>Color<input type="color" data-field="color" value="${guide.color}"></label>${numberField("Opacity %", "opacity", 0, 100)}${guide.kind !== "grid" ? `<div class="comty-guide-align"><span>Type</span><span data-guide-align></span></div>` : ""}${numberField(guide.kind === "grid" ? "Size" : guide.kind === "rows" ? "Height" : "Width", "size", 1, 4096)}${numberField("Offset", "offset", 0, 4096)}${guide.kind !== "grid" ? numberField("Gutter", "gutter", 0, 800) : ""}`;
3598
+ if (guide.kind !== "grid") {
3599
+ const alignment = guideSelect("Guide alignment", [["stretch", "Stretch"], ["start", "Start"], ["center", "Center"], ["end", "End"]], guide.align, value => {
3600
+ guide.align = value;
3601
+ renderLayoutGuides();
3602
+ });
3603
+ fields.querySelector("[data-guide-align]").replaceWith(alignment.trigger);
3604
+ }
3074
3605
  fields.oninput = (event) => {
3075
3606
  const input = event.target, key = input.dataset.field;
3076
3607
  if (!key) return;
@@ -3086,7 +3617,7 @@ function initComty(options = {}) {
3086
3617
  };
3087
3618
  item.querySelector("[data-edit]").onclick = (event) => {
3088
3619
  selectedGuideId = guide.id;
3089
- typeSelect.value = guide.kind;
3620
+ typeSelect.setValue(guide.kind);
3090
3621
  fields.hidden = !fields.hidden;
3091
3622
  event.currentTarget.setAttribute("aria-expanded", String(!fields.hidden));
3092
3623
  };
@@ -3106,12 +3637,14 @@ function initComty(options = {}) {
3106
3637
 
3107
3638
  function syncInspectorTools() {
3108
3639
  const visible = session.inspect;
3640
+ measurements.setEnabled(visible && rulersEnabled && !motion.enabled);
3109
3641
  overlayTools.hidden = !visible;
3110
3642
  xrayBtn.setAttribute("aria-pressed", String(visible && xrayEnabled));
3111
3643
  rulersBtn.setAttribute("aria-pressed", String(visible && rulersEnabled));
3112
3644
  gridBtn.setAttribute("aria-pressed", String(visible && layoutGuides.some(guide => guide.visible)));
3113
3645
  gridBtn.setAttribute("aria-expanded", String(visible && gridPanelOpen));
3114
3646
  toolPanel.hidden = !visible || !gridPanelOpen;
3647
+ if (toolPanel.hidden) closeGuideMenu();
3115
3648
  rulerLayer.hidden = !visible || !rulersEnabled;
3116
3649
  if (visible && xrayEnabled) {
3117
3650
  if (!xrayStyle.isConnected) document.head.appendChild(xrayStyle);
@@ -3237,45 +3770,221 @@ function initComty(options = {}) {
3237
3770
  }
3238
3771
  document.addEventListener("pointercancel", cancelRulerDrag, true);
3239
3772
 
3240
- function animateColorToast(closing = false) {
3241
- colorToastAnimation?.cancel();
3242
- colorToastAnimation = null;
3243
- if (toolPanel.hidden) return;
3244
- if (reducedMotion.matches || !toolPanel.classList.contains("comty-color-toast")) {
3245
- if (closing) toolPanel.hidden = true;
3246
- return;
3773
+ function colorDetails(value, token = "") {
3774
+ const channels = /^#([\da-f]{6})$/i.exec(value)
3775
+ ? value.slice(1).match(/../g).map(part => parseInt(part, 16))
3776
+ : /^rgba?\((.*)\)$/i.exec(value)?.[1].replace(/\s*\/\s*/g, " ").split(/[\s,]+/).filter(Boolean);
3777
+ if (!channels || channels.length < 3) return null;
3778
+ const [r, g, b] = channels.slice(0, 3).map(part => Math.max(0, Math.min(255, Math.round(part.endsWith?.("%") ? parseFloat(part) * 2.55 : Number(part)))));
3779
+ if (![r, g, b].every(Number.isFinite)) return null;
3780
+ const alpha = channels[3] == null ? 1 : Math.max(0, Math.min(1, channels[3].endsWith("%") ? parseFloat(channels[3]) / 100 : Number(channels[3])));
3781
+ const hex = `#${[r, g, b].map(n => n.toString(16).padStart(2, "0")).join("").toUpperCase()}${alpha < 1 ? Math.round(alpha * 255).toString(16).padStart(2, "0").toUpperCase() : ""}`;
3782
+ const [red, green, blue] = [r, g, b].map(n => n / 255);
3783
+ const max = Math.max(red, green, blue), min = Math.min(red, green, blue), delta = max - min;
3784
+ let hue = 0;
3785
+ if (delta) hue = max === red ? ((green - blue) / delta) % 6 : max === green ? (blue - red) / delta + 2 : (red - green) / delta + 4;
3786
+ hue = (hue * 60 + 360) % 360;
3787
+ const lightness = (max + min) / 2;
3788
+ const saturation = delta ? delta / (1 - Math.abs(2 * lightness - 1)) : 0;
3789
+ const linear = [red, green, blue].map(n => n <= .04045 ? n / 12.92 : ((n + .055) / 1.055) ** 2.4);
3790
+ const l = Math.cbrt(.4122214708 * linear[0] + .5363325363 * linear[1] + .0514459929 * linear[2]);
3791
+ const m = Math.cbrt(.2119034982 * linear[0] + .6806995451 * linear[1] + .1073969566 * linear[2]);
3792
+ const s = Math.cbrt(.0883024619 * linear[0] + .2817188376 * linear[1] + .6299787005 * linear[2]);
3793
+ const L = .2104542553 * l + .793617785 * m - .0040720468 * s;
3794
+ const a = 1.9779984951 * l - 2.428592205 * m + .4505937099 * s;
3795
+ const bLab = .0259040371 * l + .7827717662 * m - .808675766 * s;
3796
+ const chroma = Math.hypot(a, bLab);
3797
+ const okHue = chroma < .001 ? 0 : (Math.atan2(bLab, a) * 180 / Math.PI + 360) % 360;
3798
+ const suffix = alpha < 1 ? ` / ${Number(alpha.toFixed(2))}` : "";
3799
+ return {
3800
+ key: `${hex}|${token}`, hex, token,
3801
+ name: ({ "#FFFFFF": "White", "#000000": "Black" })[hex] || (token ? token.replace(/^--/, "").replace(/[-_]/g, " ") : "Selected colour"),
3802
+ rgb: alpha < 1 ? `rgba(${r}, ${g}, ${b}, ${Number(alpha.toFixed(2))})` : `rgb(${r}, ${g}, ${b})`,
3803
+ hsl: `hsl(${Math.round(hue)} ${Math.round(saturation * 100)}% ${Math.round(lightness * 100)}%${suffix})`,
3804
+ oklch: `oklch(${(L * 100).toFixed(1)}% ${chroma.toFixed(3)} ${Math.round(okHue)}${suffix})`,
3805
+ };
3806
+ }
3807
+
3808
+ function fontDetails(element) {
3809
+ const css = getComputedStyle(element);
3810
+ const color = colorDetails(css.color);
3811
+ const values = [css.fontFamily, css.fontSize, css.fontWeight, css.lineHeight, css.letterSpacing, css.color];
3812
+ return {
3813
+ key: values.join("|"), family: css.fontFamily,
3814
+ size: css.fontSize, weight: css.fontWeight, leading: css.lineHeight,
3815
+ tracking: css.letterSpacing === "normal" ? "0px" : css.letterSpacing,
3816
+ color,
3817
+ css: `font-family: ${css.fontFamily};\nfont-size: ${css.fontSize};\nfont-weight: ${css.fontWeight};\nline-height: ${css.lineHeight};\nletter-spacing: ${css.letterSpacing};\ncolor: ${css.color};`,
3818
+ };
3819
+ }
3820
+
3821
+ function rememberProperty(list, item) {
3822
+ const previous = list.findIndex(entry => entry.key === item.key);
3823
+ if (previous !== -1) list.splice(previous, 1);
3824
+ list.unshift(item);
3825
+ list.length = Math.min(list.length, 4);
3826
+ }
3827
+
3828
+ 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
+ const checkIcon = icon('<path d="m5 12 4 4 10-10"/>');
3830
+ const copyFeedbackTimers = new WeakMap();
3831
+ function propertyRow(label, value, display = value) {
3832
+ const unavailable = value === "—";
3833
+ 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>`;
3834
+ }
3835
+
3836
+ function propertyVisibleRect() {
3837
+ const rect = propertyPanel.getBoundingClientRect();
3838
+ const inset = getComputedStyle(propertyPanel).clipPath.match(/^inset\(([^)]+?)(?: round|\))/)?.[1].split(/\s+/).map(parseFloat) || [0];
3839
+ const [top, right = top, bottom = top, left = right] = inset;
3840
+ return { top: rect.top + top, right: rect.right - right, bottom: rect.bottom - bottom, left: rect.left + left };
3841
+ }
3842
+
3843
+ function renderPropertyPanel(morph = false) {
3844
+ if (!propertyKind) return;
3845
+ const before = morph && !propertyPanel.hidden ? propertyVisibleRect() : null;
3846
+ const oldDetail = propertyPanel.querySelector(".comty-property-detail")?.cloneNode(true);
3847
+ const oldStrip = propertyPanel.querySelector(".comty-property-strip > :first-child")?.getBoundingClientRect();
3848
+ propertyAnimation?.cancel(); propertyAnimation = null;
3849
+ propertyStripAnimation?.cancel(); propertyStripAnimation = null;
3850
+ propertyPanel.style.width = "";
3851
+ const expanded = Boolean(propertySelection);
3852
+ propertyPanel.dataset.kind = propertyKind;
3853
+ propertyPanel.toggleAttribute("data-expanded", expanded);
3854
+ propertyPanel.setAttribute("aria-label", propertyKind === "color" ? "Recent colours" : "Recent typography");
3855
+ const recent = propertyKind === "color" ? recentColors : recentFonts;
3856
+ const details = propertyKind === "color" && expanded
3857
+ ? `<div class="comty-property-detail"><div class="comty-property-rows">${propertyRow("HEX", propertySelection.hex)}${propertyRow("RGB", propertySelection.rgb)}${propertyRow("HSL", propertySelection.hsl)}${propertyRow("OKLCH", propertySelection.oklch)}${propertyRow("Token", propertySelection.token ? `var(${propertySelection.token})` : "—")}</div></div>`
3858
+ : propertyKind === "font" && expanded
3859
+ ? `<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
+ : "";
3861
+ const slots = Array.from({ length: 4 }, (_, index) => {
3862
+ const item = recent[index];
3863
+ if (!item) return '<span class="comty-property-empty" aria-hidden="true"></span>';
3864
+ const label = propertyKind === "color" ? `${item.name}, ${item.hex}` : `Font ${index + 1}: ${item.family}`;
3865
+ return `<button type="button" class="comty-property-recent" data-recent="${index}" aria-label="${escapeInspectorText(label)}" aria-pressed="${String(propertySelection?.key === item.key)}">${propertyKind === "font" ? "Aa" : ""}</button>`;
3866
+ }).join("");
3867
+ propertyPanel.innerHTML = `${details}<div class="comty-property-strip">${slots}</div><span class="comty-property-feedback" role="status"></span>`;
3868
+ propertyPanel.hidden = false;
3869
+ if (propertyKind === "color") {
3870
+ propertyPanel.querySelectorAll(".comty-property-recent").forEach((button, index) => { button.style.backgroundColor = recent[index].hex; });
3871
+ } else {
3872
+ propertyPanel.querySelectorAll(".comty-property-recent").forEach((button, index) => { button.style.fontFamily = recent[index].family; button.style.fontWeight = recent[index].weight; });
3873
+ const colourRow = propertyPanel.querySelector('.comty-property-row button[aria-label^="Copy Colour"]');
3874
+ if (colourRow && propertySelection.color) colourRow.querySelector("span").style.setProperty("--comty-picked-color", propertySelection.color.hex);
3247
3875
  }
3248
3876
  positionToolbar();
3249
- const above = toolPanel.getBoundingClientRect().bottom <= toolbar.getBoundingClientRect().top;
3250
- toolPanel.style.transformOrigin = above ? "bottom right" : "top right";
3251
- const content = toolPanel.firstChild;
3252
- const style = getComputedStyle(root);
3253
- const animation = toolPanel.animate([
3254
- { opacity: 0, transform: `translateY(${above ? ".375rem" : "-.375rem"}) scale(.96)` },
3255
- { opacity: 1, transform: "translateY(0) scale(1)" },
3256
- ], {
3257
- duration: parseFloat(style.getPropertyValue("--comty-motion-duration")),
3258
- easing: style.getPropertyValue("--comty-ease-out").trim(),
3259
- direction: closing ? "reverse" : "normal",
3260
- fill: "both",
3261
- });
3262
- colorToastAnimation = animation;
3877
+ if (reducedMotion.matches || !pointerInput || !morph || !before) return;
3878
+ const target = propertyPanel.getBoundingClientRect();
3879
+ const targetStrip = propertyPanel.querySelector(".comty-property-strip > :first-child").getBoundingClientRect();
3880
+ // Keep the outgoing detail until the frame has contracted around the recent values.
3881
+ if (!expanded && oldDetail) {
3882
+ propertyPanel.prepend(oldDetail);
3883
+ propertyPanel.style.width = getComputedStyle(root).getPropertyValue("--comty-property-width").trim();
3884
+ positionToolbar();
3885
+ }
3886
+ const layout = propertyPanel.getBoundingClientRect();
3887
+ const strip = propertyPanel.querySelector(".comty-property-strip");
3888
+ const stripRect = strip.firstElementChild.getBoundingClientRect();
3889
+ const radius = getComputedStyle(propertyPanel).borderRadius;
3890
+ const clip = rect => `inset(${Math.max(0, rect.top - layout.top)}px ${Math.max(0, layout.right - rect.right)}px ${Math.max(0, layout.bottom - rect.bottom)}px ${Math.max(0, rect.left - layout.left)}px round ${radius})`;
3891
+ const css = getComputedStyle(root);
3892
+ const timing = { duration: parseFloat(css.getPropertyValue("--comty-motion-duration")), easing: css.getPropertyValue("--comty-ease-in-out").trim(), fill: "forwards" };
3893
+ propertyAnimation = propertyPanel.animate([{ clipPath: clip(before) }, { clipPath: clip(target) }], timing);
3894
+ propertyStripAnimation = strip.animate([
3895
+ { transform: `translate(${oldStrip.left - stripRect.left}px, ${oldStrip.top - stripRect.top}px)` },
3896
+ { transform: `translate(${targetStrip.left - stripRect.left}px, ${targetStrip.top - stripRect.top}px)` },
3897
+ ], timing);
3898
+ const animation = propertyAnimation;
3263
3899
  animation.onfinish = () => {
3264
- if (colorToastAnimation !== animation) return;
3265
- if (closing && toolPanel.firstChild === content) toolPanel.hidden = true;
3900
+ if (propertyAnimation !== animation) return;
3901
+ propertyAnimation = null;
3902
+ if (!expanded) { oldDetail?.remove(); propertyPanel.style.width = ""; }
3903
+ propertyStripAnimation?.cancel(); propertyStripAnimation = null;
3266
3904
  animation.cancel();
3267
- colorToastAnimation = null;
3905
+ positionToolbar();
3268
3906
  };
3269
3907
  }
3270
3908
 
3271
- function dismissColorResultLater() {
3272
- clearTimeout(colorToastTimer);
3273
- const content = toolPanel.firstChild;
3274
- colorToastTimer = setTimeout(() => {
3275
- if (toolPanel.firstChild === content) animateColorToast(true);
3276
- }, 4000);
3909
+ function openPropertyPanel(kind, element, property, opener = null, sampledColor = null) {
3910
+ propertyPicking = null;
3911
+ colorMagnifier.setEnabled(false);
3912
+ delete document.documentElement.dataset.comtyPropertyPicking;
3913
+ if (motion.enabled) setMotionEnabled(false);
3914
+ gridPanelOpen = false;
3915
+ toolPanel.hidden = true;
3916
+ gridBtn.setAttribute("aria-expanded", "false");
3917
+ const item = kind === "color" && element
3918
+ ? sampledColor ? colorDetails(sampledColor, inspectedBackgroundToken(sampledColor, element) || "") : colorDetails(getComputedStyle(element)[property === "background" ? "backgroundColor" : "color"], getColorTokens(element)[property === "background" ? "background" : "color"].at(0) || "")
3919
+ : kind === "font" && element ? fontDetails(element) : null;
3920
+ if (item) rememberProperty(kind === "color" ? recentColors : recentFonts, item);
3921
+ propertyOpener = opener;
3922
+ propertyKind = kind;
3923
+ propertySelection = null;
3924
+ for (const [button, active] of [[pipetteBtn, kind === "color"], [typographyBtn, kind === "font"]]) {
3925
+ button.setAttribute("aria-pressed", String(active));
3926
+ button.setAttribute("aria-expanded", String(active));
3927
+ }
3928
+ renderPropertyPanel();
3929
+ }
3930
+
3931
+ function togglePropertyPanel(kind, opener, screenSampling = false) {
3932
+ if (!screenSampling && !propertyPanel.hidden && propertyKind === kind) { closePropertyPanel(); return; }
3933
+ openPropertyPanel(kind, null, "color", opener);
3934
+ rulersEnabled = false;
3935
+ syncInspectorTools();
3936
+ if (kind === "color" && screenSampling && window.EyeDropper) sampleColor();
3937
+ else {
3938
+ propertyPicking = kind;
3939
+ colorMagnifier.setEnabled(kind === "color");
3940
+ document.documentElement.dataset.comtyPropertyPicking = kind;
3941
+ selectedElement = null;
3942
+ hideInspector();
3943
+ }
3277
3944
  }
3278
3945
 
3946
+ function closePropertyPanel(restoreFocus = false) {
3947
+ if (propertyPanel.hidden) return;
3948
+ if (propertyPicking === "font") hideInspector();
3949
+ propertyAnimation?.cancel(); propertyAnimation = null;
3950
+ propertyStripAnimation?.cancel(); propertyStripAnimation = null;
3951
+ colorPickerController?.abort();
3952
+ propertyPanel.hidden = true;
3953
+ propertyKind = null; propertySelection = null; propertyPicking = null;
3954
+ colorMagnifier.setEnabled(false);
3955
+ delete document.documentElement.dataset.comtyPropertyPicking;
3956
+ for (const button of [pipetteBtn, typographyBtn]) {
3957
+ button.setAttribute("aria-pressed", "false");
3958
+ button.setAttribute("aria-expanded", "false");
3959
+ }
3960
+ if (restoreFocus && propertyOpener?.isConnected) propertyOpener.focus({ preventScroll: true });
3961
+ propertyOpener = null;
3962
+ }
3963
+
3964
+ propertyPanel.onclick = async event => {
3965
+ const button = event.target.closest("button");
3966
+ if (!button) return;
3967
+ if (button.dataset.recent != null) {
3968
+ const item = (propertyKind === "color" ? recentColors : recentFonts)[Number(button.dataset.recent)];
3969
+ propertySelection = propertySelection?.key === item.key ? null : item;
3970
+ renderPropertyPanel(true);
3971
+ propertyPanel.querySelector(`button[data-recent="${button.dataset.recent}"]`)?.focus({ preventScroll: true });
3972
+ } else if (button.dataset.copy != null && button.dataset.copy !== "—") {
3973
+ clearTimeout(copyFeedbackTimers.get(button));
3974
+ try {
3975
+ await navigator.clipboard.writeText(button.dataset.copy);
3976
+ if (!button.isConnected || propertyPanel.hidden) return;
3977
+ button.dataset.copied = "";
3978
+ propertyPanel.querySelector(".comty-property-feedback").textContent = `${button.getAttribute("aria-label").slice(5)} copied`;
3979
+ clearTimeout(copyFeedbackTimers.get(button));
3980
+ copyFeedbackTimers.set(button, setTimeout(() => { delete button.dataset.copied; }, 1200));
3981
+ } catch {
3982
+ delete button.dataset.copied;
3983
+ if (button.isConnected && !propertyPanel.hidden) propertyPanel.querySelector(".comty-property-feedback").textContent = "Clipboard unavailable";
3984
+ }
3985
+ }
3986
+ };
3987
+
3279
3988
  function inspectedBackgroundToken(hex, element) {
3280
3989
  if (!element?.isConnected || isElementTreeHidden(element)) return null;
3281
3990
  const sampled = `rgb(${hex.slice(1).match(/../g).map(part => parseInt(part, 16)).join(', ')})`;
@@ -3293,53 +4002,42 @@ function initComty(options = {}) {
3293
4002
  async function sampleColor() {
3294
4003
  if (colorPickerController) return;
3295
4004
  const inspectedBackground = selectedElement?.isConnected && !isElementTreeHidden(selectedElement) ? selectedElement : inspectedElement;
3296
- setMotionEnabled(false);
3297
- gridPanelOpen = false;
3298
- renderGridPanel();
3299
- toolPanel.hidden = false;
3300
- toolPanel.setAttribute("aria-label", "Color sampler");
3301
4005
  if (!window.EyeDropper) {
3302
- toolPanel.textContent = "Screen color sampling is not supported by this browser. Open this page in Chrome or Edge.";
3303
- dismissColorResultLater();
4006
+ propertyPanel.querySelector(".comty-property-feedback").textContent = "Screen sampling requires Chrome or Edge";
3304
4007
  return;
3305
4008
  }
3306
4009
  colorPickerController = new AbortController();
3307
- pipetteBtn.setAttribute("aria-pressed", "true");
3308
4010
  // Hide every overlay so the sampler reads the page's original pixels.
3309
4011
  const previousVisibility = root.style.visibility;
3310
4012
  root.style.visibility = "hidden";
3311
4013
  xrayStyle.remove();
3312
4014
  try {
3313
4015
  const { sRGBHex } = await new window.EyeDropper().open({ signal: colorPickerController.signal });
4016
+ if (colorPickerController.signal.aborted || propertyKind !== "color") return;
3314
4017
  if (!/^#[0-9a-f]{6}$/i.test(sRGBHex)) throw new Error("Invalid sampled color");
3315
- const rgb = sRGBHex.slice(1).match(/../g).map(part => parseInt(part, 16));
3316
4018
  const token = inspectedBackgroundToken(sRGBHex, inspectedBackground);
3317
- toolPanel.classList.add("comty-color-toast");
3318
- toolPanel.innerHTML = `<div class="comty-color-content">${token ? `<div class="comty-color-token">Element token · ${token}</div>` : ""}<div class="comty-color-values"><span class="comty-color-hex">${sRGBHex.toUpperCase()}</span><span class="comty-color-separator" aria-hidden="true">–</span><span class="comty-color-rgb">rgb(${rgb.join(", ")})</span></div><div class="comty-color-status" role="status">Copying…</div></div><span class="comty-color-swatch" aria-hidden="true"></span>`;
3319
- toolPanel.querySelector(".comty-color-swatch").style.backgroundColor = sRGBHex;
3320
- const status = toolPanel.querySelector(".comty-color-status");
3321
- dismissColorResultLater();
3322
- try {
3323
- await navigator.clipboard.writeText(sRGBHex.toUpperCase());
3324
- status.textContent = "Copied";
3325
- } catch { status.textContent = "Select to copy"; }
4019
+ const item = colorDetails(sRGBHex, token || "");
4020
+ rememberProperty(recentColors, item);
4021
+ propertyKind = "color";
4022
+ propertySelection = null;
4023
+ renderPropertyPanel();
4024
+ try { await navigator.clipboard.writeText(item.hex); propertyPanel.querySelector(".comty-property-feedback").textContent = "HEX copied"; }
4025
+ catch { propertyPanel.querySelector(".comty-property-feedback").textContent = "Select a value to copy"; }
3326
4026
  } catch (error) {
3327
- if (error.name === "AbortError") toolPanel.hidden = true;
3328
- else { toolPanel.textContent = "Unable to sample a color. Please try again."; dismissColorResultLater(); }
4027
+ if (error.name !== "AbortError") propertyPanel.querySelector(".comty-property-feedback").textContent = "Unable to sample a colour";
3329
4028
  } finally {
3330
4029
  root.style.visibility = previousVisibility;
3331
- if (toolPanel.classList.contains("comty-color-toast")) animateColorToast();
3332
4030
  colorPickerController = null;
3333
- pipetteBtn.setAttribute("aria-pressed", "false");
3334
4031
  if (session.inspect && xrayEnabled) document.head.appendChild(xrayStyle);
3335
4032
  }
3336
4033
  }
3337
4034
  document.addEventListener("pointerdown", event => {
3338
4035
  if (!toolPanel.hidden && !toolPanel.contains(event.target) && !toolbar.contains(event.target)) {
3339
4036
  gridPanelOpen = false;
3340
- animateColorToast(true);
4037
+ toolPanel.hidden = true;
3341
4038
  gridBtn.setAttribute("aria-expanded", "false");
3342
4039
  }
4040
+ if (!propertyPicking && !propertyPanel.hidden && !propertyPanel.contains(event.target) && !pipetteBtn.contains(event.target) && !typographyBtn.contains(event.target) && !event.target.closest?.(".comty-inspector-picker")) closePropertyPanel();
3343
4041
  }, true);
3344
4042
 
3345
4043
  const sidepanelBtn = document.createElement("button");
@@ -3400,11 +4098,56 @@ function initComty(options = {}) {
3400
4098
  let toolbarMotionFrame = 0;
3401
4099
  let toolbarAnimations = [];
3402
4100
  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);
3403
4145
  const clampToolbar = (value, size, viewport) => Math.max(8, Math.min(value, viewport - size - 8));
3404
4146
  function rememberToolbarPosition(left, top) {
3405
4147
  const width = toolbar.offsetWidth, height = toolbar.offsetHeight;
3406
4148
  const centerX = left + width / 2, centerY = top + height / 2;
3407
- const horizontal = centerX < innerWidth / 3 ? "left" : centerX > innerWidth * 2 / 3 ? "right" : "center";
4149
+ const availableWidth = innerWidth - width;
4150
+ const horizontal = left < availableWidth / 3 ? "left" : left > availableWidth * 2 / 3 ? "right" : "center";
3408
4151
  const vertical = centerY < innerHeight / 2 ? "top" : "bottom";
3409
4152
  toolbarPosition = {
3410
4153
  horizontal, vertical,
@@ -3432,20 +4175,26 @@ function initComty(options = {}) {
3432
4175
  });
3433
4176
  }
3434
4177
  const rect = toolbar.getBoundingClientRect();
3435
- for (const panel of [toolPanel, sidepanel, motionPanel]) {
4178
+ for (const panel of [toolPanel, propertyPanel, sidepanel, motionPanel]) {
3436
4179
  if (panel.hidden || (panel === sidepanel && !sidepanelOpen)) continue;
3437
4180
  const above = rect.top - 16;
3438
4181
  const below = innerHeight - rect.bottom - 16;
3439
- const useAbove = above >= panel.scrollHeight || above >= below;
4182
+ const useAbove = panel === propertyPanel ? above >= below : above >= panel.scrollHeight || above >= below;
3440
4183
  panel.style.maxHeight = `${Math.max(0, useAbove ? above : below)}px`;
3441
- panel.style.left = `${Math.max(8, Math.min(rect.right - panel.offsetWidth, innerWidth - panel.offsetWidth - 8))}px`;
4184
+ const edgeThreshold = toolbar.offsetHeight;
4185
+ const alignment = rect.left <= edgeThreshold ? "left" : innerWidth - rect.right <= edgeThreshold ? "right" : "center";
4186
+ const left = panel === propertyPanel
4187
+ ? alignment === "left" ? rect.left : alignment === "right" ? rect.right - panel.offsetWidth : rect.left + (rect.width - panel.offsetWidth) / 2
4188
+ : rect.right - panel.offsetWidth;
4189
+ if (panel === propertyPanel) panel.dataset.alignment = alignment;
4190
+ panel.style.left = `${Math.max(8, Math.min(left, innerWidth - panel.offsetWidth - 8))}px`;
3442
4191
  panel.style.right = "auto";
3443
4192
  panel.style.top = `${useAbove ? rect.top - panel.offsetHeight - 8 : rect.bottom + 8}px`;
3444
4193
  panel.style.bottom = "auto";
3445
4194
  }
3446
4195
  }
3447
4196
  const toolbarResize = new ResizeObserver(positionToolbar);
3448
- [toolbar, toolPanel, sidepanel, motionPanel].forEach(node => toolbarResize.observe(node));
4197
+ [toolbar, toolPanel, propertyPanel, sidepanel, motionPanel].forEach(node => toolbarResize.observe(node));
3449
4198
  window.addEventListener("resize", positionToolbar);
3450
4199
 
3451
4200
  toolbar.addEventListener("pointerdown", event => {
@@ -3505,8 +4254,11 @@ function initComty(options = {}) {
3505
4254
 
3506
4255
  function prepareToolbarMotion() {
3507
4256
  if (toolbarMotionFrame) return;
3508
- const nodes = [toolbarSurface, browseBtn, inspectorBtn, btn, agentBtn, divider, commentTools, agentTools, inspectorTools].filter(Boolean);
3509
- const before = new Map(nodes.filter(node => !node.hidden).map(node => [node, node.getBoundingClientRect()]));
4257
+ const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, agentTools, inspectorTools].filter(Boolean);
4258
+ // Move each group's background with its icons, including when interrupted.
4259
+ const before = new Map(nodes.filter(node => !node.hidden).map(node => [node, {
4260
+ rect: node.getBoundingClientRect(), opacity: getComputedStyle(node).opacity,
4261
+ }]));
3510
4262
  stopToolbarMotion();
3511
4263
  toolbarMotionFrame = requestAnimationFrame(() => {
3512
4264
  toolbarMotionFrame = 0;
@@ -3516,8 +4268,8 @@ function initComty(options = {}) {
3516
4268
  const timing = { duration: parseFloat(css.getPropertyValue("--comty-motion-duration")), easing: css.getPropertyValue("--comty-ease-out").trim() };
3517
4269
  for (const node of nodes.filter(node => !node.hidden)) {
3518
4270
  const next = node.getBoundingClientRect(), previous = before.get(node);
3519
- const transform = previous ? `translate(${previous.left - next.left}px, ${previous.top - next.top}px)${node === toolbarSurface ? ` scale(${previous.width / next.width}, ${previous.height / next.height})` : ""}` : "translateY(.25rem)";
3520
- toolbarAnimations.push(node.animate([{ transform, opacity: previous ? 1 : 0 }, { transform: "none", opacity: 1 }], timing));
4271
+ const transform = previous ? `translate(${previous.rect.left - next.left}px, ${previous.rect.top - next.top}px)${node === toolbarSurface ? ` scale(${previous.rect.width / next.width}, ${previous.rect.height / next.height})` : ""}` : "none";
4272
+ toolbarAnimations.push(node.animate([{ transform, opacity: previous?.opacity ?? 0 }, { transform: "none", opacity: 1 }], timing));
3521
4273
  }
3522
4274
  });
3523
4275
  }
@@ -3714,8 +4466,10 @@ function initComty(options = {}) {
3714
4466
  if (mode === "off" || mode === "inspect" || session.inspect) {
3715
4467
  if (mode === "inspect" && session.inspect) return;
3716
4468
  setMotionEnabled(false);
4469
+ closePropertyPanel();
3717
4470
  colorPickerController?.abort();
3718
4471
  cancelRulerDrag();
4472
+ rulersEnabled = false;
3719
4473
  gridPanelOpen = false;
3720
4474
  toolPanel.hidden = true;
3721
4475
  measuredRegion = null;
@@ -3775,31 +4529,52 @@ function initComty(options = {}) {
3775
4529
  return;
3776
4530
  }
3777
4531
  document.querySelector(".comty-auth-backdrop")?.remove();
4532
+ const opener = document.activeElement;
3778
4533
  const backdrop = document.createElement("div");
3779
4534
  backdrop.className = "comty-auth-backdrop";
3780
4535
  backdrop.innerHTML = `
3781
- <div class="comty-auth-dialog" role="dialog" aria-modal="true" aria-labelledby="comty-auth-title">
4536
+ <div class="comty-auth-dialog" role="dialog" aria-modal="true" aria-labelledby="comty-auth-title" aria-describedby="comty-auth-description" tabindex="-1">
4537
+ <div class="comty-auth-header">
4538
+ <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
+ <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
+ </div>
3782
4541
  <h2 id="comty-auth-title">${forbidden ? "Project access required" : "Sign in to comment"}</h2>
3783
- <p>${forbidden ? "This account is not a member of this project. Ask the project owner for an invite." : "Comments are shared with the project team, so we need to know who is writing."}</p>
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>
3784
4543
  <div class="comty-auth-actions">
3785
- <button type="button" data-comty-close>Close</button>
4544
+ <button type="button" data-comty-close>${forbidden ? "Close" : "Cancel"}</button>
3786
4545
  ${forbidden ? "" : '<button type="button" class="comty-auth-primary" data-comty-login>Continue to sign in</button>'}
3787
4546
  </div>
3788
4547
  </div>`;
3789
- backdrop.querySelector("[data-comty-close]").onclick = () => backdrop.remove();
4548
+ const close = () => {
4549
+ backdrop.remove();
4550
+ if (opener?.isConnected) opener.focus({ preventScroll: true });
4551
+ };
4552
+ backdrop.querySelectorAll("[data-comty-close]").forEach(button => { button.onclick = close; });
4553
+ backdrop.addEventListener("keydown", event => {
4554
+ event.stopPropagation();
4555
+ if (event.key === "Escape") { event.preventDefault(); close(); }
4556
+ if (event.key === "Tab") {
4557
+ const buttons = [...backdrop.querySelectorAll("button:not(:disabled)")];
4558
+ if (!buttons.length) { event.preventDefault(); return; }
4559
+ const first = buttons[0], last = buttons.at(-1);
4560
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
4561
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
4562
+ }
4563
+ });
3790
4564
  const loginButton = backdrop.querySelector("[data-comty-login]");
3791
4565
  if (loginButton) {
3792
4566
  loginButton.onclick = () => {
3793
4567
  loginButton.disabled = true;
3794
4568
  loginButton.innerText = "Connecting…";
3795
- backdrop.querySelector("[data-comty-close]").disabled = true;
4569
+ backdrop.querySelectorAll("[data-comty-close]").forEach(button => { button.disabled = true; });
3796
4570
  setTimeout(() => window.location.assign(loginUrl), 50);
3797
4571
  };
3798
4572
  }
3799
4573
  backdrop.onclick = (event) => {
3800
- if (event.target === backdrop) backdrop.remove();
4574
+ if (event.target === backdrop) close();
3801
4575
  };
3802
4576
  root.appendChild(backdrop);
4577
+ (loginButton || backdrop.querySelector("[data-comty-close]")).focus({ preventScroll: true });
3803
4578
  }
3804
4579
 
3805
4580
  function formatBoxValue(value) {
@@ -3909,7 +4684,7 @@ function initComty(options = {}) {
3909
4684
  };
3910
4685
  try {
3911
4686
  for (const sheet of [...document.styleSheets, ...document.adoptedStyleSheets]) {
3912
- if (sheet.disabled || sheet.ownerNode?.closest?.('#comty-root') || sheet.ownerNode === style) continue;
4687
+ if (sheet.disabled || sheet.ownerNode?.closest?.('#comty-root') || sheet.ownerNode === style || sheet.ownerNode?.matches?.('link[data-comty-font]')) continue;
3913
4688
  if (sheet.media?.mediaText && !matchMedia(sheet.media.mediaText).matches) continue;
3914
4689
  visit(sheet.cssRules);
3915
4690
  }
@@ -4114,7 +4889,29 @@ function initComty(options = {}) {
4114
4889
  }
4115
4890
 
4116
4891
  function renderInspector() {
4117
- if (motion.enabled) {
4892
+ if (propertyPicking === "font") {
4893
+ if (!inspectedElement?.isConnected || isElementTreeHidden(inspectedElement)) { hideInspector(); return; }
4894
+ let box = inspectionLayer.querySelector(".comty-font-target");
4895
+ if (!box) {
4896
+ box = document.createElement("div");
4897
+ box.className = "comty-font-target";
4898
+ inspectionLayer.replaceChildren(box);
4899
+ }
4900
+ const rect = inspectedElement.getBoundingClientRect();
4901
+ // Tight line-height can leave glyphs outside the element's layout box.
4902
+ const range = document.createRange();
4903
+ range.selectNodeContents(inspectedElement);
4904
+ const text = range.getBoundingClientRect();
4905
+ const left = Math.min(rect.left, text.width ? text.left : rect.left);
4906
+ const top = Math.min(rect.top, text.height ? text.top : rect.top);
4907
+ setMeasuredRect(box, {
4908
+ left, top,
4909
+ width: Math.max(rect.right, text.width ? text.right : rect.right) - left,
4910
+ height: Math.max(rect.bottom, text.height ? text.bottom : rect.bottom) - top,
4911
+ });
4912
+ return;
4913
+ }
4914
+ if (propertyPicking || motion.enabled || (session.inspect && rulersEnabled)) {
4118
4915
  if (inspectionLayer.childNodes.length) hideInspector();
4119
4916
  return;
4120
4917
  }
@@ -4189,13 +4986,26 @@ function initComty(options = {}) {
4189
4986
  panel.setAttribute("aria-label", "Element properties");
4190
4987
  if (selected) panel.dataset.selected = "";
4191
4988
  const copyable = (value) => `<button type="button" title="Copy ${escapeInspectorText(value)}" data-copy="${escapeInspectorText(value)}">${escapeInspectorText(value)}</button>`;
4192
- panel.innerHTML = `<div class="comty-inspector-head">${copyable(selector)}${copyable(`${formatBoxValue(rect.width)} × ${formatBoxValue(rect.height)}`)}</div><div class="comty-inspector-details">${(inspectExpanded ? rows : rows.slice(0, 6)).map(([key, value, tokens]) => `<div class="comty-inspector-row"><span>${key}</span><div class="comty-inspector-value">${tokens?.length ? `<div class="comty-inspector-tokens">${tokens.map(token => copyable(token)).join("")}</div>` : ""}${copyable(value)}</div></div>`).join("")}</div>${rows.length > 6 && selected ? `<button type="button" class="comty-inspector-expand">${inspectExpanded ? "Show less" : `Show ${rows.length - 6} more`}</button>` : ""}`;
4989
+ const featured = new Set(["background", "color", "font", "font-family"]);
4990
+ const visibleRows = inspectExpanded ? rows : selected ? [...rows.filter(([key]) => featured.has(key)), ...rows.filter(([key]) => !featured.has(key))].slice(0, 6) : rows.slice(0, 6);
4991
+ panel.innerHTML = `<div class="comty-inspector-head">${copyable(selector)}${copyable(`${formatBoxValue(rect.width)} × ${formatBoxValue(rect.height)}`)}</div><div class="comty-inspector-details">${visibleRows.map(([key, value, tokens]) => {
4992
+ const picker = key === "background" || key === "color" ? "color" : key === "font" || key === "font-family" ? "font" : "";
4993
+ const valueButton = picker && selected
4994
+ ? `<button type="button" class="comty-inspector-picker" data-picker="${picker}" data-property="${key}" aria-label="Open ${picker === "color" ? key + " colour" : "typography"} details">${picker === "color" ? '<span class="comty-inspector-swatch" aria-hidden="true"></span>' : ""}${escapeInspectorText(value)}</button>`
4995
+ : copyable(value);
4996
+ return `<div class="comty-inspector-row"><span>${key}</span><div class="comty-inspector-value">${tokens?.length ? `<div class="comty-inspector-tokens">${tokens.map(token => copyable(token)).join("")}</div>` : ""}${valueButton}</div></div>`;
4997
+ }).join("")}</div>${rows.length > 6 && selected ? `<button type="button" class="comty-inspector-expand">${inspectExpanded ? "Show less" : `Show ${rows.length - 6} more`}</button>` : ""}`;
4998
+ for (const button of panel.querySelectorAll('.comty-inspector-picker[data-picker="color"]')) {
4999
+ button.querySelector(".comty-inspector-swatch").style.backgroundColor = getComputedStyle(target)[button.dataset.property === "background" ? "backgroundColor" : "color"];
5000
+ }
4193
5001
  panel.onclick = async (event) => {
4194
5002
  const button = event.target.closest("button");
4195
5003
  if (!button) return;
4196
5004
  if (button.classList.contains("comty-inspector-expand")) {
4197
5005
  inspectExpanded = !inspectExpanded;
4198
5006
  renderInspector();
5007
+ } else if (button.dataset.picker) {
5008
+ openPropertyPanel(button.dataset.picker, target, button.dataset.property, button);
4199
5009
  } else {
4200
5010
  try {
4201
5011
  await navigator.clipboard.writeText(button.dataset.copy);
@@ -5343,7 +6153,8 @@ function initComty(options = {}) {
5343
6153
  const failed = Boolean(task.error) || task.status === 'in_review';
5344
6154
  const state = failed ? 'error' : task.status === 'done' ? 'success' : task.status === 'cancel' ? 'cancel' : 'working';
5345
6155
  const row = node('div', 'comty-agent-row'); row.dataset.taskId = task.id;
5346
- const dot = node('span', 'comty-agent-dot'); dot.dataset.status = state; dot.setAttribute('aria-hidden', 'true');
6156
+ const dot = state === 'working' ? createWorkingIndicator() : node('span', 'comty-agent-dot');
6157
+ dot.dataset.status = state; dot.setAttribute('aria-hidden', 'true');
5347
6158
  const name = node('span', 'comty-agent-row-name', task.content);
5348
6159
  row.append(dot, name);
5349
6160
  if (state === 'working') row.appendChild(node('span', 'comty-agent-row-state', task.status === 'todo' ? 'Starting' : 'Working'));
@@ -6197,7 +7008,9 @@ function initComty(options = {}) {
6197
7008
  outline = document.createElement("div");
6198
7009
  outline.className = "comty-agent-working";
6199
7010
  outline.setAttribute("aria-label", "Agent working on this element");
6200
- const badge = document.createElement("span"); badge.textContent = "Working";
7011
+ const badge = document.createElement("span");
7012
+ badge.className = "comty-agent-working-badge";
7013
+ badge.append(createWorkingIndicator(), "Working");
6201
7014
  outline.appendChild(badge); root.appendChild(outline);
6202
7015
  workingOutlines.set(id, outline);
6203
7016
  }
@@ -6219,6 +7032,7 @@ function initComty(options = {}) {
6219
7032
  }
6220
7033
  if (session.inspect || session.edit || inspectionLayer.childNodes.length) renderInspector();
6221
7034
  if (session.inspect && rulersEnabled) renderRulers();
7035
+ if (session.inspect && rulersEnabled) measurements.render();
6222
7036
  }
6223
7037
 
6224
7038
  function ensureLiveTracking() {
@@ -6310,6 +7124,14 @@ function initComty(options = {}) {
6310
7124
  document.addEventListener("pointermove", (event) => {
6311
7125
  if (!session.edit && !session.inspect) return;
6312
7126
  if (motion.enabled) return;
7127
+ if (propertyPicking) {
7128
+ const target = event.target;
7129
+ if (propertyPicking === "font" && target instanceof Element && !isComtyElement(target) && getElementOwnTextSignature(target)) {
7130
+ inspectedElement = target;
7131
+ renderInspector();
7132
+ } else hideInspector();
7133
+ return;
7134
+ }
6313
7135
  if (inspectDrag) {
6314
7136
  const dx = event.clientX - inspectDrag.x, dy = event.clientY - inspectDrag.y;
6315
7137
  if (Math.hypot(dx, dy) > 4) {
@@ -6341,7 +7163,7 @@ function initComty(options = {}) {
6341
7163
  if ((!session.inspect && !session.edit) || !(event.target instanceof Element) || isComtyElement(event.target)) return;
6342
7164
  if (event.button !== 0) return;
6343
7165
  if (motion.enabled) return;
6344
- if (session.inspect) {
7166
+ if (session.inspect && !propertyPicking) {
6345
7167
  inspectDrag = { x: event.clientX, y: event.clientY };
6346
7168
  measuredRegion = null;
6347
7169
  }
@@ -6371,6 +7193,19 @@ function initComty(options = {}) {
6371
7193
  e.preventDefault();
6372
7194
  e.stopImmediatePropagation();
6373
7195
  if (ignoreInspectClick) return;
7196
+ if (propertyPicking) {
7197
+ const kind = propertyPicking;
7198
+ let element = target;
7199
+ const property = getElementOwnTextSignature(element) ? "color" : "background";
7200
+ if (kind === "color" && property === "background") {
7201
+ while (element.parentElement && getComputedStyle(element).backgroundColor === "rgba(0, 0, 0, 0)") element = element.parentElement;
7202
+ }
7203
+ openPropertyPanel(kind, element, property, kind === "color" ? pipetteBtn : typographyBtn, kind === "color" ? colorMagnifier.sample() : null);
7204
+ propertyPicking = kind;
7205
+ colorMagnifier.setEnabled(kind === "color");
7206
+ document.documentElement.dataset.comtyPropertyPicking = kind;
7207
+ return;
7208
+ }
6374
7209
  measuredRegion = null;
6375
7210
  selectedElement = getAnchorElement(target);
6376
7211
  inspectExpanded = false;
@@ -6442,15 +7277,16 @@ function initComty(options = {}) {
6442
7277
  setMotionEnabled(false); motionBtn.focus({ preventScroll: true });
6443
7278
  return;
6444
7279
  }
6445
- if (e.key === "Escape" && (!toolPanel.hidden || rulerDrag || colorPickerController)) {
7280
+ if (e.key === "Escape" && (!propertyPanel.hidden || !toolPanel.hidden || rulerDrag || colorPickerController)) {
6446
7281
  e.preventDefault();
6447
- gridPanelOpen = false; animateColorToast(true);
7282
+ closePropertyPanel(true);
7283
+ gridPanelOpen = false; toolPanel.hidden = true;
6448
7284
  gridBtn.setAttribute("aria-expanded", "false");
6449
7285
  cancelRulerDrag(); colorPickerController?.abort();
6450
7286
  return;
6451
7287
  }
6452
7288
  if (session.inspect && !isEditable && !e.repeat && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
6453
- const button = { x: xrayBtn, r: rulersBtn, g: gridBtn, p: pipetteBtn, m: motionBtn }[e.key.toLowerCase()];
7289
+ const button = { x: xrayBtn, r: rulersBtn, g: gridBtn, p: pipetteBtn, t: typographyBtn, m: motionBtn }[e.key.toLowerCase()];
6454
7290
  if (button) { e.preventDefault(); button.click(); return; }
6455
7291
  }
6456
7292