@comty.io/core 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -29
- package/comty-sdk.js +1046 -210
- package/package.json +2 -2
- package/src/browser.js +531 -99
- package/src/capture.js +104 -1
- package/src/dev.mjs +3 -2
- package/src/measurements.js +209 -0
- package/src/style.js +203 -111
- package/src/vite.mjs +3 -2
package/src/browser.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getComtyStyle } from "./style.js";
|
|
2
2
|
import { createAnchorMatcher } from "./anchors.js";
|
|
3
|
-
import { createScreenshotCapture } from "./capture.js";
|
|
3
|
+
import { createScreenshotCapture, createColorMagnifier } from "./capture.js";
|
|
4
4
|
import { createCommentThread } from "./comments.js";
|
|
5
|
+
import { createMeasurements } from "./measurements.js";
|
|
5
6
|
// Shared browser runtime. Both npm and the standalone CDN script use this file.
|
|
6
7
|
const instanceKey = Symbol.for("comty.sdk.instance");
|
|
7
8
|
|
|
@@ -139,6 +140,13 @@ export function initComty(options = {}) {
|
|
|
139
140
|
const style = document.createElement("style");
|
|
140
141
|
style.innerHTML = getComtyStyle();
|
|
141
142
|
document.head.appendChild(style);
|
|
143
|
+
if (!document.querySelector('link[data-comty-font]')) {
|
|
144
|
+
const font = document.createElement('link');
|
|
145
|
+
font.rel = 'stylesheet';
|
|
146
|
+
font.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap';
|
|
147
|
+
font.dataset.comtyFont = '';
|
|
148
|
+
document.head.appendChild(font);
|
|
149
|
+
}
|
|
142
150
|
|
|
143
151
|
const toolbar = document.createElement("div");
|
|
144
152
|
toolbar.className = "comty-toolbar";
|
|
@@ -152,6 +160,12 @@ export function initComty(options = {}) {
|
|
|
152
160
|
toolbarSurface.className = "comty-toolbar-surface";
|
|
153
161
|
toolbar.appendChild(toolbarSurface);
|
|
154
162
|
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>`;
|
|
163
|
+
const dragHandle = document.createElement("span");
|
|
164
|
+
dragHandle.className = "comty-toolbar-grip";
|
|
165
|
+
dragHandle.title = "Drag to move";
|
|
166
|
+
dragHandle.setAttribute("aria-hidden", "true");
|
|
167
|
+
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>');
|
|
168
|
+
toolbar.appendChild(dragHandle);
|
|
155
169
|
function toolButton(label, paths, action) {
|
|
156
170
|
const button = document.createElement("button");
|
|
157
171
|
button.className = "comty-control-button";
|
|
@@ -199,6 +213,10 @@ export function initComty(options = {}) {
|
|
|
199
213
|
agentHistoryBtn.setAttribute("aria-expanded", "false");
|
|
200
214
|
agentTools.appendChild(agentHistoryBtn);
|
|
201
215
|
}
|
|
216
|
+
const primaryTools = document.createElement("div");
|
|
217
|
+
primaryTools.className = "comty-tool-primary";
|
|
218
|
+
toolbar.insertBefore(primaryTools, browseBtn);
|
|
219
|
+
for (const button of [browseBtn, inspectorBtn, btn, agentBtn].filter(Boolean)) primaryTools.appendChild(button);
|
|
202
220
|
const inspectorTools = document.createElement("div");
|
|
203
221
|
inspectorTools.className = "comty-tool-context";
|
|
204
222
|
inspectorTools.hidden = true;
|
|
@@ -212,11 +230,24 @@ export function initComty(options = {}) {
|
|
|
212
230
|
let rulerDrag = null;
|
|
213
231
|
const rulerGuides = [];
|
|
214
232
|
let colorPickerController = null;
|
|
215
|
-
|
|
216
|
-
|
|
233
|
+
const recentColors = [];
|
|
234
|
+
const recentFonts = [];
|
|
235
|
+
let propertyKind = null;
|
|
236
|
+
let propertySelection = null;
|
|
237
|
+
let propertyOpener = null;
|
|
238
|
+
let propertyAnimation = null;
|
|
239
|
+
let propertyStripAnimation = null;
|
|
240
|
+
let propertyPicking = null;
|
|
241
|
+
const colorMagnifier = createColorMagnifier({ root, isComtyElement, capture: () => captureViewportScreenshot() });
|
|
217
242
|
const overlayTools = document.createElement("div");
|
|
218
243
|
overlayTools.className = "comty-overlay-tools";
|
|
219
244
|
root.prepend(overlayTools);
|
|
245
|
+
const measurements = createMeasurements({
|
|
246
|
+
root,
|
|
247
|
+
isComtyElement,
|
|
248
|
+
isElementTreeHidden,
|
|
249
|
+
onExit() { rulersEnabled = false; syncInspectorTools(); },
|
|
250
|
+
});
|
|
220
251
|
const gridLayer = document.createElement("div");
|
|
221
252
|
gridLayer.className = "comty-layout-guides";
|
|
222
253
|
overlayTools.appendChild(gridLayer);
|
|
@@ -228,6 +259,11 @@ export function initComty(options = {}) {
|
|
|
228
259
|
toolPanel.className = "comty-tool-panel";
|
|
229
260
|
toolPanel.hidden = true;
|
|
230
261
|
root.appendChild(toolPanel);
|
|
262
|
+
const propertyPanel = document.createElement("section");
|
|
263
|
+
propertyPanel.className = "comty-property-panel";
|
|
264
|
+
propertyPanel.id = "comty-property-panel";
|
|
265
|
+
propertyPanel.hidden = true;
|
|
266
|
+
root.appendChild(propertyPanel);
|
|
231
267
|
const xrayStyle = document.createElement("style");
|
|
232
268
|
// Use an outline overlay: geometry and the page's existing styles stay intact.
|
|
233
269
|
xrayStyle.textContent = `body *:not(#comty-root):not(#comty-root *) { outline: 1px solid ${getComputedStyle(root).getPropertyValue('--comty-xray').trim()} !important; }`;
|
|
@@ -236,10 +272,17 @@ export function initComty(options = {}) {
|
|
|
236
272
|
syncInspectorTools();
|
|
237
273
|
});
|
|
238
274
|
const rulersBtn = toolButton("Rulers · R", '<path d="M3 3h18v6H9v12H3Z"/><path d="M7 3v3m4-3v3m4-3v3M3 11h3m-3 4h3m-3 4h3"/>', () => {
|
|
275
|
+
closePropertyPanel();
|
|
276
|
+
if (motion.enabled) setMotionEnabled(false);
|
|
239
277
|
rulersEnabled = !rulersEnabled;
|
|
278
|
+
selectedElement = null;
|
|
279
|
+
measuredRegion = null;
|
|
280
|
+
inspectDrag = null;
|
|
281
|
+
hideInspector();
|
|
240
282
|
syncInspectorTools();
|
|
241
283
|
});
|
|
242
284
|
const gridBtn = toolButton("Layout guides · G", '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 10h18M10 3v18"/>', () => {
|
|
285
|
+
closePropertyPanel();
|
|
243
286
|
if (motion.enabled) setMotionEnabled(false);
|
|
244
287
|
gridPanelOpen = !gridPanelOpen;
|
|
245
288
|
if (gridPanelOpen && !layoutGuides.length) addLayoutGuide();
|
|
@@ -247,10 +290,16 @@ export function initComty(options = {}) {
|
|
|
247
290
|
syncInspectorTools();
|
|
248
291
|
});
|
|
249
292
|
gridBtn.setAttribute("aria-expanded", "false");
|
|
250
|
-
const pipetteBtn = toolButton("Sample color · P", '<path d="m16 3 5 5-4 4-5-5Zm-3 5L4 17v3h3l9-9M11 6l7 7"/>',
|
|
293
|
+
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));
|
|
294
|
+
pipetteBtn.title = "Sample color · P · Shift-click to sample the screen";
|
|
295
|
+
const typographyBtn = toolButton("Typography · T", '<path d="m5 20 7-16 7 16M8 14h8M3 20h5m8 0h5M10 4h4"/>', () => togglePropertyPanel("font", typographyBtn));
|
|
296
|
+
for (const button of [pipetteBtn, typographyBtn]) {
|
|
297
|
+
button.setAttribute("aria-controls", propertyPanel.id);
|
|
298
|
+
button.setAttribute("aria-expanded", "false");
|
|
299
|
+
}
|
|
251
300
|
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));
|
|
252
301
|
motionBtn.setAttribute("aria-expanded", "false");
|
|
253
|
-
for (const button of [xrayBtn, rulersBtn, gridBtn, pipetteBtn, motionBtn]) inspectorTools.appendChild(button);
|
|
302
|
+
for (const button of [xrayBtn, rulersBtn, gridBtn, pipetteBtn, typographyBtn, motionBtn]) inspectorTools.appendChild(button);
|
|
254
303
|
|
|
255
304
|
const motion = { enabled: false, speed: 1, paused: false, managed: new Map(), shadowRoots: [], nextRootScan: 0 };
|
|
256
305
|
const motionPanel = document.createElement('section');
|
|
@@ -299,6 +348,9 @@ export function initComty(options = {}) {
|
|
|
299
348
|
motion.enabled = enabled; motionPanel.hidden = !enabled;
|
|
300
349
|
motionBtn.setAttribute('aria-pressed', String(enabled)); motionBtn.setAttribute('aria-expanded', String(enabled));
|
|
301
350
|
if (enabled) {
|
|
351
|
+
closePropertyPanel();
|
|
352
|
+
rulersEnabled = false;
|
|
353
|
+
syncInspectorTools();
|
|
302
354
|
gridPanelOpen = false; toolPanel.hidden = true; gridBtn.setAttribute('aria-expanded', 'false');
|
|
303
355
|
measuredRegion = null; inspectDrag = null; selectedElement = null; hideInspector();
|
|
304
356
|
motion.nextRootScan = 0; updateMotion(); renderMotionPanel(); positionToolbar(); ensureLiveTracking();
|
|
@@ -402,32 +454,101 @@ export function initComty(options = {}) {
|
|
|
402
454
|
}
|
|
403
455
|
}
|
|
404
456
|
|
|
457
|
+
let closeGuideMenu = () => {};
|
|
458
|
+
function guideSelect(label, options, value, onChange) {
|
|
459
|
+
const trigger = document.createElement("button");
|
|
460
|
+
trigger.type = "button";
|
|
461
|
+
trigger.className = "comty-guide-select";
|
|
462
|
+
trigger.setAttribute("aria-label", label);
|
|
463
|
+
trigger.setAttribute("aria-haspopup", "menu");
|
|
464
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
465
|
+
function setValue(next) {
|
|
466
|
+
trigger.value = next;
|
|
467
|
+
trigger.innerHTML = `${options.find(option => option[0] === next)[1]}${icon('<path d="m8 10 4 4 4-4"/>')}`;
|
|
468
|
+
}
|
|
469
|
+
setValue(value);
|
|
470
|
+
function open() {
|
|
471
|
+
const wasOpen = trigger.getAttribute("aria-expanded") === "true";
|
|
472
|
+
closeGuideMenu();
|
|
473
|
+
if (wasOpen) return;
|
|
474
|
+
root.querySelectorAll('.comty-popover').forEach(popover => popover.remove());
|
|
475
|
+
const menu = document.createElement("div");
|
|
476
|
+
menu.className = "comty-popover comty-guide-popover";
|
|
477
|
+
menu.setAttribute("role", "menu");
|
|
478
|
+
menu.setAttribute("aria-label", label);
|
|
479
|
+
const controller = new AbortController();
|
|
480
|
+
const close = (focus = false) => {
|
|
481
|
+
controller.abort(); menu.remove();
|
|
482
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
483
|
+
if (focus && trigger.isConnected) trigger.focus();
|
|
484
|
+
closeGuideMenu = () => {};
|
|
485
|
+
};
|
|
486
|
+
closeGuideMenu = close;
|
|
487
|
+
const items = options.map(([key, name]) => {
|
|
488
|
+
const item = document.createElement("button");
|
|
489
|
+
item.type = "button";
|
|
490
|
+
item.className = "comty-popover-item";
|
|
491
|
+
item.setAttribute("role", "menuitemradio");
|
|
492
|
+
item.setAttribute("aria-checked", String(key === trigger.value));
|
|
493
|
+
item.textContent = name;
|
|
494
|
+
if (key === trigger.value) {
|
|
495
|
+
const check = document.createElement("span");
|
|
496
|
+
check.className = "comty-popover-item-check";
|
|
497
|
+
check.innerHTML = icon('<path d="m5 12 4 4 10-10"/>');
|
|
498
|
+
item.appendChild(check);
|
|
499
|
+
}
|
|
500
|
+
item.onclick = () => { close(); setValue(key); onChange(key); if (trigger.isConnected) trigger.focus(); };
|
|
501
|
+
menu.appendChild(item);
|
|
502
|
+
return item;
|
|
503
|
+
});
|
|
504
|
+
toolPanel.appendChild(menu);
|
|
505
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
506
|
+
const rect = trigger.getBoundingClientRect();
|
|
507
|
+
menu.style.left = `${Math.max(8, Math.min(rect.right - menu.offsetWidth, innerWidth - menu.offsetWidth - 8))}px`;
|
|
508
|
+
menu.style.top = `${rect.bottom + menu.offsetHeight + 4 <= innerHeight - 8 ? rect.bottom + 4 : Math.max(8, rect.top - menu.offsetHeight - 4)}px`;
|
|
509
|
+
items[options.findIndex(option => option[0] === trigger.value)].focus();
|
|
510
|
+
menu.onkeydown = event => {
|
|
511
|
+
const index = items.indexOf(document.activeElement);
|
|
512
|
+
if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
|
|
513
|
+
event.preventDefault(); event.stopPropagation();
|
|
514
|
+
items[event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + items.length) % items.length].focus();
|
|
515
|
+
} else if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(true); }
|
|
516
|
+
else if (event.key === "Tab") close();
|
|
517
|
+
};
|
|
518
|
+
document.addEventListener("pointerdown", event => {
|
|
519
|
+
if (!menu.contains(event.target) && !trigger.contains(event.target)) close();
|
|
520
|
+
}, {capture: true, signal: controller.signal});
|
|
521
|
+
window.addEventListener("resize", () => close(), {signal: controller.signal});
|
|
522
|
+
}
|
|
523
|
+
trigger.onclick = open;
|
|
524
|
+
trigger.onkeydown = event => {
|
|
525
|
+
if (["ArrowDown", "ArrowUp"].includes(event.key)) { event.preventDefault(); open(); }
|
|
526
|
+
};
|
|
527
|
+
return { trigger, setValue };
|
|
528
|
+
}
|
|
529
|
+
|
|
405
530
|
function renderGridPanel() {
|
|
406
|
-
|
|
407
|
-
colorToastAnimation?.cancel();
|
|
408
|
-
colorToastAnimation = null;
|
|
409
|
-
toolPanel.classList.remove("comty-color-toast");
|
|
531
|
+
closeGuideMenu();
|
|
410
532
|
toolPanel.hidden = !gridPanelOpen || !session.inspect;
|
|
411
533
|
gridBtn.setAttribute("aria-expanded", String(gridPanelOpen && session.inspect));
|
|
412
534
|
if (toolPanel.hidden) return;
|
|
413
535
|
toolPanel.setAttribute("aria-label", "Layout guides");
|
|
414
|
-
toolPanel.innerHTML = `<div class="comty-tool-panel-header"><strong>Layout guides</strong><
|
|
536
|
+
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>`;
|
|
415
537
|
const selectedGuide = layoutGuides.find(guide => guide.id === selectedGuideId) || layoutGuides[0];
|
|
416
538
|
selectedGuideId = selectedGuide?.id ?? null;
|
|
417
|
-
const typeSelect =
|
|
418
|
-
typeSelect.value = selectedGuide?.kind ?? "columns";
|
|
419
|
-
typeSelect.onchange = () => {
|
|
539
|
+
const typeSelect = guideSelect("Guide type", [["columns", "Columns"], ["rows", "Rows"], ["grid", "Grid"]], selectedGuide?.kind ?? "columns", value => {
|
|
420
540
|
const guide = layoutGuides.find(guide => guide.id === selectedGuideId);
|
|
421
541
|
if (guide) {
|
|
422
|
-
if ((guide.kind === "grid") !== (
|
|
423
|
-
guide.kind =
|
|
424
|
-
} else addLayoutGuide(
|
|
542
|
+
if ((guide.kind === "grid") !== (value === "grid")) guide.size = value === "grid" ? 8 : 72;
|
|
543
|
+
guide.kind = value;
|
|
544
|
+
} else addLayoutGuide(value);
|
|
425
545
|
renderGridPanel();
|
|
426
546
|
syncInspectorTools();
|
|
427
|
-
toolPanel.querySelector("
|
|
428
|
-
};
|
|
547
|
+
toolPanel.querySelector('[aria-label="Guide type"]').focus();
|
|
548
|
+
});
|
|
549
|
+
toolPanel.querySelector("[data-guide-type]").replaceWith(typeSelect.trigger);
|
|
429
550
|
toolPanel.querySelector("[data-add]").onclick = () => {
|
|
430
|
-
addLayoutGuide(
|
|
551
|
+
addLayoutGuide(typeSelect.trigger.value);
|
|
431
552
|
renderGridPanel();
|
|
432
553
|
};
|
|
433
554
|
const list = toolPanel.querySelector(".comty-guide-list");
|
|
@@ -440,8 +561,14 @@ export function initComty(options = {}) {
|
|
|
440
561
|
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>`;
|
|
441
562
|
const fields = item.querySelector(".comty-guide-fields");
|
|
442
563
|
const numberField = (label, key, min, max) => `<label>${label}<input type="number" data-field="${key}" value="${guide[key]}" min="${min}" max="${max}" step="1"></label>`;
|
|
443
|
-
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" ? `<
|
|
444
|
-
if (guide.kind !== "grid")
|
|
564
|
+
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) : ""}`;
|
|
565
|
+
if (guide.kind !== "grid") {
|
|
566
|
+
const alignment = guideSelect("Guide alignment", [["stretch", "Stretch"], ["start", "Start"], ["center", "Center"], ["end", "End"]], guide.align, value => {
|
|
567
|
+
guide.align = value;
|
|
568
|
+
renderLayoutGuides();
|
|
569
|
+
});
|
|
570
|
+
fields.querySelector("[data-guide-align]").replaceWith(alignment.trigger);
|
|
571
|
+
}
|
|
445
572
|
fields.oninput = (event) => {
|
|
446
573
|
const input = event.target, key = input.dataset.field;
|
|
447
574
|
if (!key) return;
|
|
@@ -457,7 +584,7 @@ export function initComty(options = {}) {
|
|
|
457
584
|
};
|
|
458
585
|
item.querySelector("[data-edit]").onclick = (event) => {
|
|
459
586
|
selectedGuideId = guide.id;
|
|
460
|
-
typeSelect.
|
|
587
|
+
typeSelect.setValue(guide.kind);
|
|
461
588
|
fields.hidden = !fields.hidden;
|
|
462
589
|
event.currentTarget.setAttribute("aria-expanded", String(!fields.hidden));
|
|
463
590
|
};
|
|
@@ -477,12 +604,14 @@ export function initComty(options = {}) {
|
|
|
477
604
|
|
|
478
605
|
function syncInspectorTools() {
|
|
479
606
|
const visible = session.inspect;
|
|
607
|
+
measurements.setEnabled(visible && rulersEnabled && !motion.enabled);
|
|
480
608
|
overlayTools.hidden = !visible;
|
|
481
609
|
xrayBtn.setAttribute("aria-pressed", String(visible && xrayEnabled));
|
|
482
610
|
rulersBtn.setAttribute("aria-pressed", String(visible && rulersEnabled));
|
|
483
611
|
gridBtn.setAttribute("aria-pressed", String(visible && layoutGuides.some(guide => guide.visible)));
|
|
484
612
|
gridBtn.setAttribute("aria-expanded", String(visible && gridPanelOpen));
|
|
485
613
|
toolPanel.hidden = !visible || !gridPanelOpen;
|
|
614
|
+
if (toolPanel.hidden) closeGuideMenu();
|
|
486
615
|
rulerLayer.hidden = !visible || !rulersEnabled;
|
|
487
616
|
if (visible && xrayEnabled) {
|
|
488
617
|
if (!xrayStyle.isConnected) document.head.appendChild(xrayStyle);
|
|
@@ -608,45 +737,221 @@ export function initComty(options = {}) {
|
|
|
608
737
|
}
|
|
609
738
|
document.addEventListener("pointercancel", cancelRulerDrag, true);
|
|
610
739
|
|
|
611
|
-
function
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
|
|
740
|
+
function colorDetails(value, token = "") {
|
|
741
|
+
const channels = /^#([\da-f]{6})$/i.exec(value)
|
|
742
|
+
? value.slice(1).match(/../g).map(part => parseInt(part, 16))
|
|
743
|
+
: /^rgba?\((.*)\)$/i.exec(value)?.[1].replace(/\s*\/\s*/g, " ").split(/[\s,]+/).filter(Boolean);
|
|
744
|
+
if (!channels || channels.length < 3) return null;
|
|
745
|
+
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)))));
|
|
746
|
+
if (![r, g, b].every(Number.isFinite)) return null;
|
|
747
|
+
const alpha = channels[3] == null ? 1 : Math.max(0, Math.min(1, channels[3].endsWith("%") ? parseFloat(channels[3]) / 100 : Number(channels[3])));
|
|
748
|
+
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() : ""}`;
|
|
749
|
+
const [red, green, blue] = [r, g, b].map(n => n / 255);
|
|
750
|
+
const max = Math.max(red, green, blue), min = Math.min(red, green, blue), delta = max - min;
|
|
751
|
+
let hue = 0;
|
|
752
|
+
if (delta) hue = max === red ? ((green - blue) / delta) % 6 : max === green ? (blue - red) / delta + 2 : (red - green) / delta + 4;
|
|
753
|
+
hue = (hue * 60 + 360) % 360;
|
|
754
|
+
const lightness = (max + min) / 2;
|
|
755
|
+
const saturation = delta ? delta / (1 - Math.abs(2 * lightness - 1)) : 0;
|
|
756
|
+
const linear = [red, green, blue].map(n => n <= .04045 ? n / 12.92 : ((n + .055) / 1.055) ** 2.4);
|
|
757
|
+
const l = Math.cbrt(.4122214708 * linear[0] + .5363325363 * linear[1] + .0514459929 * linear[2]);
|
|
758
|
+
const m = Math.cbrt(.2119034982 * linear[0] + .6806995451 * linear[1] + .1073969566 * linear[2]);
|
|
759
|
+
const s = Math.cbrt(.0883024619 * linear[0] + .2817188376 * linear[1] + .6299787005 * linear[2]);
|
|
760
|
+
const L = .2104542553 * l + .793617785 * m - .0040720468 * s;
|
|
761
|
+
const a = 1.9779984951 * l - 2.428592205 * m + .4505937099 * s;
|
|
762
|
+
const bLab = .0259040371 * l + .7827717662 * m - .808675766 * s;
|
|
763
|
+
const chroma = Math.hypot(a, bLab);
|
|
764
|
+
const okHue = chroma < .001 ? 0 : (Math.atan2(bLab, a) * 180 / Math.PI + 360) % 360;
|
|
765
|
+
const suffix = alpha < 1 ? ` / ${Number(alpha.toFixed(2))}` : "";
|
|
766
|
+
return {
|
|
767
|
+
key: `${hex}|${token}`, hex, token,
|
|
768
|
+
name: ({ "#FFFFFF": "White", "#000000": "Black" })[hex] || (token ? token.replace(/^--/, "").replace(/[-_]/g, " ") : "Selected colour"),
|
|
769
|
+
rgb: alpha < 1 ? `rgba(${r}, ${g}, ${b}, ${Number(alpha.toFixed(2))})` : `rgb(${r}, ${g}, ${b})`,
|
|
770
|
+
hsl: `hsl(${Math.round(hue)} ${Math.round(saturation * 100)}% ${Math.round(lightness * 100)}%${suffix})`,
|
|
771
|
+
oklch: `oklch(${(L * 100).toFixed(1)}% ${chroma.toFixed(3)} ${Math.round(okHue)}${suffix})`,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function fontDetails(element) {
|
|
776
|
+
const css = getComputedStyle(element);
|
|
777
|
+
const color = colorDetails(css.color);
|
|
778
|
+
const values = [css.fontFamily, css.fontSize, css.fontWeight, css.lineHeight, css.letterSpacing, css.color];
|
|
779
|
+
return {
|
|
780
|
+
key: values.join("|"), family: css.fontFamily,
|
|
781
|
+
size: css.fontSize, weight: css.fontWeight, leading: css.lineHeight,
|
|
782
|
+
tracking: css.letterSpacing === "normal" ? "0px" : css.letterSpacing,
|
|
783
|
+
color,
|
|
784
|
+
css: `font-family: ${css.fontFamily};\nfont-size: ${css.fontSize};\nfont-weight: ${css.fontWeight};\nline-height: ${css.lineHeight};\nletter-spacing: ${css.letterSpacing};\ncolor: ${css.color};`,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function rememberProperty(list, item) {
|
|
789
|
+
const previous = list.findIndex(entry => entry.key === item.key);
|
|
790
|
+
if (previous !== -1) list.splice(previous, 1);
|
|
791
|
+
list.unshift(item);
|
|
792
|
+
list.length = Math.min(list.length, 4);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
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"/>');
|
|
796
|
+
const checkIcon = icon('<path d="m5 12 4 4 10-10"/>');
|
|
797
|
+
const copyFeedbackTimers = new WeakMap();
|
|
798
|
+
function propertyRow(label, value, display = value) {
|
|
799
|
+
const unavailable = value === "—";
|
|
800
|
+
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>`;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function propertyVisibleRect() {
|
|
804
|
+
const rect = propertyPanel.getBoundingClientRect();
|
|
805
|
+
const inset = getComputedStyle(propertyPanel).clipPath.match(/^inset\(([^)]+?)(?: round|\))/)?.[1].split(/\s+/).map(parseFloat) || [0];
|
|
806
|
+
const [top, right = top, bottom = top, left = right] = inset;
|
|
807
|
+
return { top: rect.top + top, right: rect.right - right, bottom: rect.bottom - bottom, left: rect.left + left };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function renderPropertyPanel(morph = false) {
|
|
811
|
+
if (!propertyKind) return;
|
|
812
|
+
const before = morph && !propertyPanel.hidden ? propertyVisibleRect() : null;
|
|
813
|
+
const oldDetail = propertyPanel.querySelector(".comty-property-detail")?.cloneNode(true);
|
|
814
|
+
const oldStrip = propertyPanel.querySelector(".comty-property-strip > :first-child")?.getBoundingClientRect();
|
|
815
|
+
propertyAnimation?.cancel(); propertyAnimation = null;
|
|
816
|
+
propertyStripAnimation?.cancel(); propertyStripAnimation = null;
|
|
817
|
+
propertyPanel.style.width = "";
|
|
818
|
+
const expanded = Boolean(propertySelection);
|
|
819
|
+
propertyPanel.dataset.kind = propertyKind;
|
|
820
|
+
propertyPanel.toggleAttribute("data-expanded", expanded);
|
|
821
|
+
propertyPanel.setAttribute("aria-label", propertyKind === "color" ? "Recent colours" : "Recent typography");
|
|
822
|
+
const recent = propertyKind === "color" ? recentColors : recentFonts;
|
|
823
|
+
const details = propertyKind === "color" && expanded
|
|
824
|
+
? `<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>`
|
|
825
|
+
: propertyKind === "font" && expanded
|
|
826
|
+
? `<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>`
|
|
827
|
+
: "";
|
|
828
|
+
const slots = Array.from({ length: 4 }, (_, index) => {
|
|
829
|
+
const item = recent[index];
|
|
830
|
+
if (!item) return '<span class="comty-property-empty" aria-hidden="true"></span>';
|
|
831
|
+
const label = propertyKind === "color" ? `${item.name}, ${item.hex}` : `Font ${index + 1}: ${item.family}`;
|
|
832
|
+
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>`;
|
|
833
|
+
}).join("");
|
|
834
|
+
propertyPanel.innerHTML = `${details}<div class="comty-property-strip">${slots}</div><span class="comty-property-feedback" role="status"></span>`;
|
|
835
|
+
propertyPanel.hidden = false;
|
|
836
|
+
if (propertyKind === "color") {
|
|
837
|
+
propertyPanel.querySelectorAll(".comty-property-recent").forEach((button, index) => { button.style.backgroundColor = recent[index].hex; });
|
|
838
|
+
} else {
|
|
839
|
+
propertyPanel.querySelectorAll(".comty-property-recent").forEach((button, index) => { button.style.fontFamily = recent[index].family; button.style.fontWeight = recent[index].weight; });
|
|
840
|
+
const colourRow = propertyPanel.querySelector('.comty-property-row button[aria-label^="Copy Colour"]');
|
|
841
|
+
if (colourRow && propertySelection.color) colourRow.querySelector("span").style.setProperty("--comty-picked-color", propertySelection.color.hex);
|
|
618
842
|
}
|
|
619
843
|
positionToolbar();
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
844
|
+
if (reducedMotion.matches || !pointerInput || !morph || !before) return;
|
|
845
|
+
const target = propertyPanel.getBoundingClientRect();
|
|
846
|
+
const targetStrip = propertyPanel.querySelector(".comty-property-strip > :first-child").getBoundingClientRect();
|
|
847
|
+
// Keep the outgoing detail until the frame has contracted around the recent values.
|
|
848
|
+
if (!expanded && oldDetail) {
|
|
849
|
+
propertyPanel.prepend(oldDetail);
|
|
850
|
+
propertyPanel.style.width = getComputedStyle(root).getPropertyValue("--comty-property-width").trim();
|
|
851
|
+
positionToolbar();
|
|
852
|
+
}
|
|
853
|
+
const layout = propertyPanel.getBoundingClientRect();
|
|
854
|
+
const strip = propertyPanel.querySelector(".comty-property-strip");
|
|
855
|
+
const stripRect = strip.firstElementChild.getBoundingClientRect();
|
|
856
|
+
const radius = getComputedStyle(propertyPanel).borderRadius;
|
|
857
|
+
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})`;
|
|
858
|
+
const css = getComputedStyle(root);
|
|
859
|
+
const timing = { duration: parseFloat(css.getPropertyValue("--comty-motion-duration")), easing: css.getPropertyValue("--comty-ease-in-out").trim(), fill: "forwards" };
|
|
860
|
+
propertyAnimation = propertyPanel.animate([{ clipPath: clip(before) }, { clipPath: clip(target) }], timing);
|
|
861
|
+
propertyStripAnimation = strip.animate([
|
|
862
|
+
{ transform: `translate(${oldStrip.left - stripRect.left}px, ${oldStrip.top - stripRect.top}px)` },
|
|
863
|
+
{ transform: `translate(${targetStrip.left - stripRect.left}px, ${targetStrip.top - stripRect.top}px)` },
|
|
864
|
+
], timing);
|
|
865
|
+
const animation = propertyAnimation;
|
|
634
866
|
animation.onfinish = () => {
|
|
635
|
-
if (
|
|
636
|
-
|
|
867
|
+
if (propertyAnimation !== animation) return;
|
|
868
|
+
propertyAnimation = null;
|
|
869
|
+
if (!expanded) { oldDetail?.remove(); propertyPanel.style.width = ""; }
|
|
870
|
+
propertyStripAnimation?.cancel(); propertyStripAnimation = null;
|
|
637
871
|
animation.cancel();
|
|
638
|
-
|
|
872
|
+
positionToolbar();
|
|
639
873
|
};
|
|
640
874
|
}
|
|
641
875
|
|
|
642
|
-
function
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
876
|
+
function openPropertyPanel(kind, element, property, opener = null, sampledColor = null) {
|
|
877
|
+
propertyPicking = null;
|
|
878
|
+
colorMagnifier.setEnabled(false);
|
|
879
|
+
delete document.documentElement.dataset.comtyPropertyPicking;
|
|
880
|
+
if (motion.enabled) setMotionEnabled(false);
|
|
881
|
+
gridPanelOpen = false;
|
|
882
|
+
toolPanel.hidden = true;
|
|
883
|
+
gridBtn.setAttribute("aria-expanded", "false");
|
|
884
|
+
const item = kind === "color" && element
|
|
885
|
+
? sampledColor ? colorDetails(sampledColor, inspectedBackgroundToken(sampledColor, element) || "") : colorDetails(getComputedStyle(element)[property === "background" ? "backgroundColor" : "color"], getColorTokens(element)[property === "background" ? "background" : "color"].at(0) || "")
|
|
886
|
+
: kind === "font" && element ? fontDetails(element) : null;
|
|
887
|
+
if (item) rememberProperty(kind === "color" ? recentColors : recentFonts, item);
|
|
888
|
+
propertyOpener = opener;
|
|
889
|
+
propertyKind = kind;
|
|
890
|
+
propertySelection = null;
|
|
891
|
+
for (const [button, active] of [[pipetteBtn, kind === "color"], [typographyBtn, kind === "font"]]) {
|
|
892
|
+
button.setAttribute("aria-pressed", String(active));
|
|
893
|
+
button.setAttribute("aria-expanded", String(active));
|
|
894
|
+
}
|
|
895
|
+
renderPropertyPanel();
|
|
648
896
|
}
|
|
649
897
|
|
|
898
|
+
function togglePropertyPanel(kind, opener, screenSampling = false) {
|
|
899
|
+
if (!screenSampling && !propertyPanel.hidden && propertyKind === kind) { closePropertyPanel(); return; }
|
|
900
|
+
openPropertyPanel(kind, null, "color", opener);
|
|
901
|
+
rulersEnabled = false;
|
|
902
|
+
syncInspectorTools();
|
|
903
|
+
if (kind === "color" && screenSampling && window.EyeDropper) sampleColor();
|
|
904
|
+
else {
|
|
905
|
+
propertyPicking = kind;
|
|
906
|
+
colorMagnifier.setEnabled(kind === "color");
|
|
907
|
+
document.documentElement.dataset.comtyPropertyPicking = kind;
|
|
908
|
+
selectedElement = null;
|
|
909
|
+
hideInspector();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function closePropertyPanel(restoreFocus = false) {
|
|
914
|
+
if (propertyPanel.hidden) return;
|
|
915
|
+
if (propertyPicking === "font") hideInspector();
|
|
916
|
+
propertyAnimation?.cancel(); propertyAnimation = null;
|
|
917
|
+
propertyStripAnimation?.cancel(); propertyStripAnimation = null;
|
|
918
|
+
colorPickerController?.abort();
|
|
919
|
+
propertyPanel.hidden = true;
|
|
920
|
+
propertyKind = null; propertySelection = null; propertyPicking = null;
|
|
921
|
+
colorMagnifier.setEnabled(false);
|
|
922
|
+
delete document.documentElement.dataset.comtyPropertyPicking;
|
|
923
|
+
for (const button of [pipetteBtn, typographyBtn]) {
|
|
924
|
+
button.setAttribute("aria-pressed", "false");
|
|
925
|
+
button.setAttribute("aria-expanded", "false");
|
|
926
|
+
}
|
|
927
|
+
if (restoreFocus && propertyOpener?.isConnected) propertyOpener.focus({ preventScroll: true });
|
|
928
|
+
propertyOpener = null;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
propertyPanel.onclick = async event => {
|
|
932
|
+
const button = event.target.closest("button");
|
|
933
|
+
if (!button) return;
|
|
934
|
+
if (button.dataset.recent != null) {
|
|
935
|
+
const item = (propertyKind === "color" ? recentColors : recentFonts)[Number(button.dataset.recent)];
|
|
936
|
+
propertySelection = propertySelection?.key === item.key ? null : item;
|
|
937
|
+
renderPropertyPanel(true);
|
|
938
|
+
propertyPanel.querySelector(`button[data-recent="${button.dataset.recent}"]`)?.focus({ preventScroll: true });
|
|
939
|
+
} else if (button.dataset.copy != null && button.dataset.copy !== "—") {
|
|
940
|
+
clearTimeout(copyFeedbackTimers.get(button));
|
|
941
|
+
try {
|
|
942
|
+
await navigator.clipboard.writeText(button.dataset.copy);
|
|
943
|
+
if (!button.isConnected || propertyPanel.hidden) return;
|
|
944
|
+
button.dataset.copied = "";
|
|
945
|
+
propertyPanel.querySelector(".comty-property-feedback").textContent = `${button.getAttribute("aria-label").slice(5)} copied`;
|
|
946
|
+
clearTimeout(copyFeedbackTimers.get(button));
|
|
947
|
+
copyFeedbackTimers.set(button, setTimeout(() => { delete button.dataset.copied; }, 1200));
|
|
948
|
+
} catch {
|
|
949
|
+
delete button.dataset.copied;
|
|
950
|
+
if (button.isConnected && !propertyPanel.hidden) propertyPanel.querySelector(".comty-property-feedback").textContent = "Clipboard unavailable";
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
|
|
650
955
|
function inspectedBackgroundToken(hex, element) {
|
|
651
956
|
if (!element?.isConnected || isElementTreeHidden(element)) return null;
|
|
652
957
|
const sampled = `rgb(${hex.slice(1).match(/../g).map(part => parseInt(part, 16)).join(', ')})`;
|
|
@@ -664,53 +969,42 @@ export function initComty(options = {}) {
|
|
|
664
969
|
async function sampleColor() {
|
|
665
970
|
if (colorPickerController) return;
|
|
666
971
|
const inspectedBackground = selectedElement?.isConnected && !isElementTreeHidden(selectedElement) ? selectedElement : inspectedElement;
|
|
667
|
-
setMotionEnabled(false);
|
|
668
|
-
gridPanelOpen = false;
|
|
669
|
-
renderGridPanel();
|
|
670
|
-
toolPanel.hidden = false;
|
|
671
|
-
toolPanel.setAttribute("aria-label", "Color sampler");
|
|
672
972
|
if (!window.EyeDropper) {
|
|
673
|
-
|
|
674
|
-
dismissColorResultLater();
|
|
973
|
+
propertyPanel.querySelector(".comty-property-feedback").textContent = "Screen sampling requires Chrome or Edge";
|
|
675
974
|
return;
|
|
676
975
|
}
|
|
677
976
|
colorPickerController = new AbortController();
|
|
678
|
-
pipetteBtn.setAttribute("aria-pressed", "true");
|
|
679
977
|
// Hide every overlay so the sampler reads the page's original pixels.
|
|
680
978
|
const previousVisibility = root.style.visibility;
|
|
681
979
|
root.style.visibility = "hidden";
|
|
682
980
|
xrayStyle.remove();
|
|
683
981
|
try {
|
|
684
982
|
const { sRGBHex } = await new window.EyeDropper().open({ signal: colorPickerController.signal });
|
|
983
|
+
if (colorPickerController.signal.aborted || propertyKind !== "color") return;
|
|
685
984
|
if (!/^#[0-9a-f]{6}$/i.test(sRGBHex)) throw new Error("Invalid sampled color");
|
|
686
|
-
const rgb = sRGBHex.slice(1).match(/../g).map(part => parseInt(part, 16));
|
|
687
985
|
const token = inspectedBackgroundToken(sRGBHex, inspectedBackground);
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
try {
|
|
694
|
-
|
|
695
|
-
status.textContent = "Copied";
|
|
696
|
-
} catch { status.textContent = "Select to copy"; }
|
|
986
|
+
const item = colorDetails(sRGBHex, token || "");
|
|
987
|
+
rememberProperty(recentColors, item);
|
|
988
|
+
propertyKind = "color";
|
|
989
|
+
propertySelection = null;
|
|
990
|
+
renderPropertyPanel();
|
|
991
|
+
try { await navigator.clipboard.writeText(item.hex); propertyPanel.querySelector(".comty-property-feedback").textContent = "HEX copied"; }
|
|
992
|
+
catch { propertyPanel.querySelector(".comty-property-feedback").textContent = "Select a value to copy"; }
|
|
697
993
|
} catch (error) {
|
|
698
|
-
if (error.name
|
|
699
|
-
else { toolPanel.textContent = "Unable to sample a color. Please try again."; dismissColorResultLater(); }
|
|
994
|
+
if (error.name !== "AbortError") propertyPanel.querySelector(".comty-property-feedback").textContent = "Unable to sample a colour";
|
|
700
995
|
} finally {
|
|
701
996
|
root.style.visibility = previousVisibility;
|
|
702
|
-
if (toolPanel.classList.contains("comty-color-toast")) animateColorToast();
|
|
703
997
|
colorPickerController = null;
|
|
704
|
-
pipetteBtn.setAttribute("aria-pressed", "false");
|
|
705
998
|
if (session.inspect && xrayEnabled) document.head.appendChild(xrayStyle);
|
|
706
999
|
}
|
|
707
1000
|
}
|
|
708
1001
|
document.addEventListener("pointerdown", event => {
|
|
709
1002
|
if (!toolPanel.hidden && !toolPanel.contains(event.target) && !toolbar.contains(event.target)) {
|
|
710
1003
|
gridPanelOpen = false;
|
|
711
|
-
|
|
1004
|
+
toolPanel.hidden = true;
|
|
712
1005
|
gridBtn.setAttribute("aria-expanded", "false");
|
|
713
1006
|
}
|
|
1007
|
+
if (!propertyPicking && !propertyPanel.hidden && !propertyPanel.contains(event.target) && !pipetteBtn.contains(event.target) && !typographyBtn.contains(event.target) && !event.target.closest?.(".comty-inspector-picker")) closePropertyPanel();
|
|
714
1008
|
}, true);
|
|
715
1009
|
|
|
716
1010
|
const sidepanelBtn = document.createElement("button");
|
|
@@ -771,11 +1065,56 @@ export function initComty(options = {}) {
|
|
|
771
1065
|
let toolbarMotionFrame = 0;
|
|
772
1066
|
let toolbarAnimations = [];
|
|
773
1067
|
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
1068
|
+
let workingIndicatorTimer = 0;
|
|
1069
|
+
function scheduleWorkingIndicators() {
|
|
1070
|
+
if (workingIndicatorTimer || reducedMotion.matches || document.hidden) return;
|
|
1071
|
+
workingIndicatorTimer = setTimeout(() => {
|
|
1072
|
+
workingIndicatorTimer = 0;
|
|
1073
|
+
if (!root.isConnected) return;
|
|
1074
|
+
const indicators = root.querySelectorAll('.comty-working-indicator');
|
|
1075
|
+
if (!indicators.length) return;
|
|
1076
|
+
for (const indicator of indicators) {
|
|
1077
|
+
const previous = Number(indicator.dataset.activeDots.split(',')[0]);
|
|
1078
|
+
const first = (previous + 1 + Math.floor(Math.random() * 8)) % 9;
|
|
1079
|
+
const second = (first + 1 + Math.floor(Math.random() * 8)) % 9;
|
|
1080
|
+
indicator.dataset.activeDots = `${first},${second}`;
|
|
1081
|
+
[...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === first || index === second));
|
|
1082
|
+
}
|
|
1083
|
+
scheduleWorkingIndicators();
|
|
1084
|
+
}, 600);
|
|
1085
|
+
}
|
|
1086
|
+
function createWorkingIndicator() {
|
|
1087
|
+
const indicator = document.createElement('span');
|
|
1088
|
+
indicator.className = 'comty-working-indicator';
|
|
1089
|
+
indicator.setAttribute('aria-hidden', 'true');
|
|
1090
|
+
indicator.dataset.activeDots = '3,5';
|
|
1091
|
+
for (let index = 0; index < 9; index++) {
|
|
1092
|
+
const dot = document.createElement('i');
|
|
1093
|
+
dot.toggleAttribute('data-active', index === 3 || index === 5);
|
|
1094
|
+
indicator.appendChild(dot);
|
|
1095
|
+
}
|
|
1096
|
+
scheduleWorkingIndicators();
|
|
1097
|
+
return indicator;
|
|
1098
|
+
}
|
|
1099
|
+
function resetWorkingIndicators() {
|
|
1100
|
+
clearTimeout(workingIndicatorTimer);
|
|
1101
|
+
workingIndicatorTimer = 0;
|
|
1102
|
+
if (reducedMotion.matches) {
|
|
1103
|
+
for (const indicator of root.querySelectorAll('.comty-working-indicator')) {
|
|
1104
|
+
indicator.dataset.activeDots = '3,5';
|
|
1105
|
+
[...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === 3 || index === 5));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
if (root.querySelector('.comty-working-indicator')) scheduleWorkingIndicators();
|
|
1109
|
+
}
|
|
1110
|
+
reducedMotion.addEventListener('change', resetWorkingIndicators);
|
|
1111
|
+
document.addEventListener('visibilitychange', resetWorkingIndicators);
|
|
774
1112
|
const clampToolbar = (value, size, viewport) => Math.max(8, Math.min(value, viewport - size - 8));
|
|
775
1113
|
function rememberToolbarPosition(left, top) {
|
|
776
1114
|
const width = toolbar.offsetWidth, height = toolbar.offsetHeight;
|
|
777
1115
|
const centerX = left + width / 2, centerY = top + height / 2;
|
|
778
|
-
const
|
|
1116
|
+
const availableWidth = innerWidth - width;
|
|
1117
|
+
const horizontal = left < availableWidth / 3 ? "left" : left > availableWidth * 2 / 3 ? "right" : "center";
|
|
779
1118
|
const vertical = centerY < innerHeight / 2 ? "top" : "bottom";
|
|
780
1119
|
toolbarPosition = {
|
|
781
1120
|
horizontal, vertical,
|
|
@@ -803,20 +1142,26 @@ export function initComty(options = {}) {
|
|
|
803
1142
|
});
|
|
804
1143
|
}
|
|
805
1144
|
const rect = toolbar.getBoundingClientRect();
|
|
806
|
-
for (const panel of [toolPanel, sidepanel, motionPanel]) {
|
|
1145
|
+
for (const panel of [toolPanel, propertyPanel, sidepanel, motionPanel]) {
|
|
807
1146
|
if (panel.hidden || (panel === sidepanel && !sidepanelOpen)) continue;
|
|
808
1147
|
const above = rect.top - 16;
|
|
809
1148
|
const below = innerHeight - rect.bottom - 16;
|
|
810
|
-
const useAbove = above >= panel.scrollHeight || above >= below;
|
|
1149
|
+
const useAbove = panel === propertyPanel ? above >= below : above >= panel.scrollHeight || above >= below;
|
|
811
1150
|
panel.style.maxHeight = `${Math.max(0, useAbove ? above : below)}px`;
|
|
812
|
-
|
|
1151
|
+
const edgeThreshold = toolbar.offsetHeight;
|
|
1152
|
+
const alignment = rect.left <= edgeThreshold ? "left" : innerWidth - rect.right <= edgeThreshold ? "right" : "center";
|
|
1153
|
+
const left = panel === propertyPanel
|
|
1154
|
+
? alignment === "left" ? rect.left : alignment === "right" ? rect.right - panel.offsetWidth : rect.left + (rect.width - panel.offsetWidth) / 2
|
|
1155
|
+
: rect.right - panel.offsetWidth;
|
|
1156
|
+
if (panel === propertyPanel) panel.dataset.alignment = alignment;
|
|
1157
|
+
panel.style.left = `${Math.max(8, Math.min(left, innerWidth - panel.offsetWidth - 8))}px`;
|
|
813
1158
|
panel.style.right = "auto";
|
|
814
1159
|
panel.style.top = `${useAbove ? rect.top - panel.offsetHeight - 8 : rect.bottom + 8}px`;
|
|
815
1160
|
panel.style.bottom = "auto";
|
|
816
1161
|
}
|
|
817
1162
|
}
|
|
818
1163
|
const toolbarResize = new ResizeObserver(positionToolbar);
|
|
819
|
-
[toolbar, toolPanel, sidepanel, motionPanel].forEach(node => toolbarResize.observe(node));
|
|
1164
|
+
[toolbar, toolPanel, propertyPanel, sidepanel, motionPanel].forEach(node => toolbarResize.observe(node));
|
|
820
1165
|
window.addEventListener("resize", positionToolbar);
|
|
821
1166
|
|
|
822
1167
|
toolbar.addEventListener("pointerdown", event => {
|
|
@@ -876,8 +1221,11 @@ export function initComty(options = {}) {
|
|
|
876
1221
|
|
|
877
1222
|
function prepareToolbarMotion() {
|
|
878
1223
|
if (toolbarMotionFrame) return;
|
|
879
|
-
const nodes = [toolbarSurface,
|
|
880
|
-
|
|
1224
|
+
const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, agentTools, inspectorTools].filter(Boolean);
|
|
1225
|
+
// Move each group's background with its icons, including when interrupted.
|
|
1226
|
+
const before = new Map(nodes.filter(node => !node.hidden).map(node => [node, {
|
|
1227
|
+
rect: node.getBoundingClientRect(), opacity: getComputedStyle(node).opacity,
|
|
1228
|
+
}]));
|
|
881
1229
|
stopToolbarMotion();
|
|
882
1230
|
toolbarMotionFrame = requestAnimationFrame(() => {
|
|
883
1231
|
toolbarMotionFrame = 0;
|
|
@@ -887,8 +1235,8 @@ export function initComty(options = {}) {
|
|
|
887
1235
|
const timing = { duration: parseFloat(css.getPropertyValue("--comty-motion-duration")), easing: css.getPropertyValue("--comty-ease-out").trim() };
|
|
888
1236
|
for (const node of nodes.filter(node => !node.hidden)) {
|
|
889
1237
|
const next = node.getBoundingClientRect(), previous = before.get(node);
|
|
890
|
-
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})` : ""}` : "
|
|
891
|
-
toolbarAnimations.push(node.animate([{ transform, opacity: previous
|
|
1238
|
+
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";
|
|
1239
|
+
toolbarAnimations.push(node.animate([{ transform, opacity: previous?.opacity ?? 0 }, { transform: "none", opacity: 1 }], timing));
|
|
892
1240
|
}
|
|
893
1241
|
});
|
|
894
1242
|
}
|
|
@@ -1085,8 +1433,10 @@ export function initComty(options = {}) {
|
|
|
1085
1433
|
if (mode === "off" || mode === "inspect" || session.inspect) {
|
|
1086
1434
|
if (mode === "inspect" && session.inspect) return;
|
|
1087
1435
|
setMotionEnabled(false);
|
|
1436
|
+
closePropertyPanel();
|
|
1088
1437
|
colorPickerController?.abort();
|
|
1089
1438
|
cancelRulerDrag();
|
|
1439
|
+
rulersEnabled = false;
|
|
1090
1440
|
gridPanelOpen = false;
|
|
1091
1441
|
toolPanel.hidden = true;
|
|
1092
1442
|
measuredRegion = null;
|
|
@@ -1146,31 +1496,52 @@ export function initComty(options = {}) {
|
|
|
1146
1496
|
return;
|
|
1147
1497
|
}
|
|
1148
1498
|
document.querySelector(".comty-auth-backdrop")?.remove();
|
|
1499
|
+
const opener = document.activeElement;
|
|
1149
1500
|
const backdrop = document.createElement("div");
|
|
1150
1501
|
backdrop.className = "comty-auth-backdrop";
|
|
1151
1502
|
backdrop.innerHTML = `
|
|
1152
|
-
<div class="comty-auth-dialog" role="dialog" aria-modal="true" aria-labelledby="comty-auth-title">
|
|
1503
|
+
<div class="comty-auth-dialog" role="dialog" aria-modal="true" aria-labelledby="comty-auth-title" aria-describedby="comty-auth-description" tabindex="-1">
|
|
1504
|
+
<div class="comty-auth-header">
|
|
1505
|
+
<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>
|
|
1506
|
+
<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>
|
|
1507
|
+
</div>
|
|
1153
1508
|
<h2 id="comty-auth-title">${forbidden ? "Project access required" : "Sign in to comment"}</h2>
|
|
1154
|
-
<p>${forbidden ? "This account is not a member of this project. Ask the project owner for an invite." : "
|
|
1509
|
+
<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>
|
|
1155
1510
|
<div class="comty-auth-actions">
|
|
1156
|
-
<button type="button" data-comty-close
|
|
1511
|
+
<button type="button" data-comty-close>${forbidden ? "Close" : "Cancel"}</button>
|
|
1157
1512
|
${forbidden ? "" : '<button type="button" class="comty-auth-primary" data-comty-login>Continue to sign in</button>'}
|
|
1158
1513
|
</div>
|
|
1159
1514
|
</div>`;
|
|
1160
|
-
|
|
1515
|
+
const close = () => {
|
|
1516
|
+
backdrop.remove();
|
|
1517
|
+
if (opener?.isConnected) opener.focus({ preventScroll: true });
|
|
1518
|
+
};
|
|
1519
|
+
backdrop.querySelectorAll("[data-comty-close]").forEach(button => { button.onclick = close; });
|
|
1520
|
+
backdrop.addEventListener("keydown", event => {
|
|
1521
|
+
event.stopPropagation();
|
|
1522
|
+
if (event.key === "Escape") { event.preventDefault(); close(); }
|
|
1523
|
+
if (event.key === "Tab") {
|
|
1524
|
+
const buttons = [...backdrop.querySelectorAll("button:not(:disabled)")];
|
|
1525
|
+
if (!buttons.length) { event.preventDefault(); return; }
|
|
1526
|
+
const first = buttons[0], last = buttons.at(-1);
|
|
1527
|
+
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
|
|
1528
|
+
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1161
1531
|
const loginButton = backdrop.querySelector("[data-comty-login]");
|
|
1162
1532
|
if (loginButton) {
|
|
1163
1533
|
loginButton.onclick = () => {
|
|
1164
1534
|
loginButton.disabled = true;
|
|
1165
1535
|
loginButton.innerText = "Connecting…";
|
|
1166
|
-
backdrop.
|
|
1536
|
+
backdrop.querySelectorAll("[data-comty-close]").forEach(button => { button.disabled = true; });
|
|
1167
1537
|
setTimeout(() => window.location.assign(loginUrl), 50);
|
|
1168
1538
|
};
|
|
1169
1539
|
}
|
|
1170
1540
|
backdrop.onclick = (event) => {
|
|
1171
|
-
if (event.target === backdrop)
|
|
1541
|
+
if (event.target === backdrop) close();
|
|
1172
1542
|
};
|
|
1173
1543
|
root.appendChild(backdrop);
|
|
1544
|
+
(loginButton || backdrop.querySelector("[data-comty-close]")).focus({ preventScroll: true });
|
|
1174
1545
|
}
|
|
1175
1546
|
|
|
1176
1547
|
function formatBoxValue(value) {
|
|
@@ -1280,7 +1651,7 @@ export function initComty(options = {}) {
|
|
|
1280
1651
|
};
|
|
1281
1652
|
try {
|
|
1282
1653
|
for (const sheet of [...document.styleSheets, ...document.adoptedStyleSheets]) {
|
|
1283
|
-
if (sheet.disabled || sheet.ownerNode?.closest?.('#comty-root') || sheet.ownerNode === style) continue;
|
|
1654
|
+
if (sheet.disabled || sheet.ownerNode?.closest?.('#comty-root') || sheet.ownerNode === style || sheet.ownerNode?.matches?.('link[data-comty-font]')) continue;
|
|
1284
1655
|
if (sheet.media?.mediaText && !matchMedia(sheet.media.mediaText).matches) continue;
|
|
1285
1656
|
visit(sheet.cssRules);
|
|
1286
1657
|
}
|
|
@@ -1485,7 +1856,29 @@ export function initComty(options = {}) {
|
|
|
1485
1856
|
}
|
|
1486
1857
|
|
|
1487
1858
|
function renderInspector() {
|
|
1488
|
-
if (
|
|
1859
|
+
if (propertyPicking === "font") {
|
|
1860
|
+
if (!inspectedElement?.isConnected || isElementTreeHidden(inspectedElement)) { hideInspector(); return; }
|
|
1861
|
+
let box = inspectionLayer.querySelector(".comty-font-target");
|
|
1862
|
+
if (!box) {
|
|
1863
|
+
box = document.createElement("div");
|
|
1864
|
+
box.className = "comty-font-target";
|
|
1865
|
+
inspectionLayer.replaceChildren(box);
|
|
1866
|
+
}
|
|
1867
|
+
const rect = inspectedElement.getBoundingClientRect();
|
|
1868
|
+
// Tight line-height can leave glyphs outside the element's layout box.
|
|
1869
|
+
const range = document.createRange();
|
|
1870
|
+
range.selectNodeContents(inspectedElement);
|
|
1871
|
+
const text = range.getBoundingClientRect();
|
|
1872
|
+
const left = Math.min(rect.left, text.width ? text.left : rect.left);
|
|
1873
|
+
const top = Math.min(rect.top, text.height ? text.top : rect.top);
|
|
1874
|
+
setMeasuredRect(box, {
|
|
1875
|
+
left, top,
|
|
1876
|
+
width: Math.max(rect.right, text.width ? text.right : rect.right) - left,
|
|
1877
|
+
height: Math.max(rect.bottom, text.height ? text.bottom : rect.bottom) - top,
|
|
1878
|
+
});
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (propertyPicking || motion.enabled || (session.inspect && rulersEnabled)) {
|
|
1489
1882
|
if (inspectionLayer.childNodes.length) hideInspector();
|
|
1490
1883
|
return;
|
|
1491
1884
|
}
|
|
@@ -1560,13 +1953,26 @@ export function initComty(options = {}) {
|
|
|
1560
1953
|
panel.setAttribute("aria-label", "Element properties");
|
|
1561
1954
|
if (selected) panel.dataset.selected = "";
|
|
1562
1955
|
const copyable = (value) => `<button type="button" title="Copy ${escapeInspectorText(value)}" data-copy="${escapeInspectorText(value)}">${escapeInspectorText(value)}</button>`;
|
|
1563
|
-
|
|
1956
|
+
const featured = new Set(["background", "color", "font", "font-family"]);
|
|
1957
|
+
const visibleRows = inspectExpanded ? rows : selected ? [...rows.filter(([key]) => featured.has(key)), ...rows.filter(([key]) => !featured.has(key))].slice(0, 6) : rows.slice(0, 6);
|
|
1958
|
+
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]) => {
|
|
1959
|
+
const picker = key === "background" || key === "color" ? "color" : key === "font" || key === "font-family" ? "font" : "";
|
|
1960
|
+
const valueButton = picker && selected
|
|
1961
|
+
? `<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>`
|
|
1962
|
+
: copyable(value);
|
|
1963
|
+
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>`;
|
|
1964
|
+
}).join("")}</div>${rows.length > 6 && selected ? `<button type="button" class="comty-inspector-expand">${inspectExpanded ? "Show less" : `Show ${rows.length - 6} more`}</button>` : ""}`;
|
|
1965
|
+
for (const button of panel.querySelectorAll('.comty-inspector-picker[data-picker="color"]')) {
|
|
1966
|
+
button.querySelector(".comty-inspector-swatch").style.backgroundColor = getComputedStyle(target)[button.dataset.property === "background" ? "backgroundColor" : "color"];
|
|
1967
|
+
}
|
|
1564
1968
|
panel.onclick = async (event) => {
|
|
1565
1969
|
const button = event.target.closest("button");
|
|
1566
1970
|
if (!button) return;
|
|
1567
1971
|
if (button.classList.contains("comty-inspector-expand")) {
|
|
1568
1972
|
inspectExpanded = !inspectExpanded;
|
|
1569
1973
|
renderInspector();
|
|
1974
|
+
} else if (button.dataset.picker) {
|
|
1975
|
+
openPropertyPanel(button.dataset.picker, target, button.dataset.property, button);
|
|
1570
1976
|
} else {
|
|
1571
1977
|
try {
|
|
1572
1978
|
await navigator.clipboard.writeText(button.dataset.copy);
|
|
@@ -2714,7 +3120,8 @@ export function initComty(options = {}) {
|
|
|
2714
3120
|
const failed = Boolean(task.error) || task.status === 'in_review';
|
|
2715
3121
|
const state = failed ? 'error' : task.status === 'done' ? 'success' : task.status === 'cancel' ? 'cancel' : 'working';
|
|
2716
3122
|
const row = node('div', 'comty-agent-row'); row.dataset.taskId = task.id;
|
|
2717
|
-
const dot = node('span', 'comty-agent-dot');
|
|
3123
|
+
const dot = state === 'working' ? createWorkingIndicator() : node('span', 'comty-agent-dot');
|
|
3124
|
+
dot.dataset.status = state; dot.setAttribute('aria-hidden', 'true');
|
|
2718
3125
|
const name = node('span', 'comty-agent-row-name', task.content);
|
|
2719
3126
|
row.append(dot, name);
|
|
2720
3127
|
if (state === 'working') row.appendChild(node('span', 'comty-agent-row-state', task.status === 'todo' ? 'Starting' : 'Working'));
|
|
@@ -3568,7 +3975,9 @@ export function initComty(options = {}) {
|
|
|
3568
3975
|
outline = document.createElement("div");
|
|
3569
3976
|
outline.className = "comty-agent-working";
|
|
3570
3977
|
outline.setAttribute("aria-label", "Agent working on this element");
|
|
3571
|
-
const badge = document.createElement("span");
|
|
3978
|
+
const badge = document.createElement("span");
|
|
3979
|
+
badge.className = "comty-agent-working-badge";
|
|
3980
|
+
badge.append(createWorkingIndicator(), "Working");
|
|
3572
3981
|
outline.appendChild(badge); root.appendChild(outline);
|
|
3573
3982
|
workingOutlines.set(id, outline);
|
|
3574
3983
|
}
|
|
@@ -3590,6 +3999,7 @@ export function initComty(options = {}) {
|
|
|
3590
3999
|
}
|
|
3591
4000
|
if (session.inspect || session.edit || inspectionLayer.childNodes.length) renderInspector();
|
|
3592
4001
|
if (session.inspect && rulersEnabled) renderRulers();
|
|
4002
|
+
if (session.inspect && rulersEnabled) measurements.render();
|
|
3593
4003
|
}
|
|
3594
4004
|
|
|
3595
4005
|
function ensureLiveTracking() {
|
|
@@ -3681,6 +4091,14 @@ export function initComty(options = {}) {
|
|
|
3681
4091
|
document.addEventListener("pointermove", (event) => {
|
|
3682
4092
|
if (!session.edit && !session.inspect) return;
|
|
3683
4093
|
if (motion.enabled) return;
|
|
4094
|
+
if (propertyPicking) {
|
|
4095
|
+
const target = event.target;
|
|
4096
|
+
if (propertyPicking === "font" && target instanceof Element && !isComtyElement(target) && getElementOwnTextSignature(target)) {
|
|
4097
|
+
inspectedElement = target;
|
|
4098
|
+
renderInspector();
|
|
4099
|
+
} else hideInspector();
|
|
4100
|
+
return;
|
|
4101
|
+
}
|
|
3684
4102
|
if (inspectDrag) {
|
|
3685
4103
|
const dx = event.clientX - inspectDrag.x, dy = event.clientY - inspectDrag.y;
|
|
3686
4104
|
if (Math.hypot(dx, dy) > 4) {
|
|
@@ -3712,7 +4130,7 @@ export function initComty(options = {}) {
|
|
|
3712
4130
|
if ((!session.inspect && !session.edit) || !(event.target instanceof Element) || isComtyElement(event.target)) return;
|
|
3713
4131
|
if (event.button !== 0) return;
|
|
3714
4132
|
if (motion.enabled) return;
|
|
3715
|
-
if (session.inspect) {
|
|
4133
|
+
if (session.inspect && !propertyPicking) {
|
|
3716
4134
|
inspectDrag = { x: event.clientX, y: event.clientY };
|
|
3717
4135
|
measuredRegion = null;
|
|
3718
4136
|
}
|
|
@@ -3742,6 +4160,19 @@ export function initComty(options = {}) {
|
|
|
3742
4160
|
e.preventDefault();
|
|
3743
4161
|
e.stopImmediatePropagation();
|
|
3744
4162
|
if (ignoreInspectClick) return;
|
|
4163
|
+
if (propertyPicking) {
|
|
4164
|
+
const kind = propertyPicking;
|
|
4165
|
+
let element = target;
|
|
4166
|
+
const property = getElementOwnTextSignature(element) ? "color" : "background";
|
|
4167
|
+
if (kind === "color" && property === "background") {
|
|
4168
|
+
while (element.parentElement && getComputedStyle(element).backgroundColor === "rgba(0, 0, 0, 0)") element = element.parentElement;
|
|
4169
|
+
}
|
|
4170
|
+
openPropertyPanel(kind, element, property, kind === "color" ? pipetteBtn : typographyBtn, kind === "color" ? colorMagnifier.sample() : null);
|
|
4171
|
+
propertyPicking = kind;
|
|
4172
|
+
colorMagnifier.setEnabled(kind === "color");
|
|
4173
|
+
document.documentElement.dataset.comtyPropertyPicking = kind;
|
|
4174
|
+
return;
|
|
4175
|
+
}
|
|
3745
4176
|
measuredRegion = null;
|
|
3746
4177
|
selectedElement = getAnchorElement(target);
|
|
3747
4178
|
inspectExpanded = false;
|
|
@@ -3813,15 +4244,16 @@ export function initComty(options = {}) {
|
|
|
3813
4244
|
setMotionEnabled(false); motionBtn.focus({ preventScroll: true });
|
|
3814
4245
|
return;
|
|
3815
4246
|
}
|
|
3816
|
-
if (e.key === "Escape" && (!toolPanel.hidden || rulerDrag || colorPickerController)) {
|
|
4247
|
+
if (e.key === "Escape" && (!propertyPanel.hidden || !toolPanel.hidden || rulerDrag || colorPickerController)) {
|
|
3817
4248
|
e.preventDefault();
|
|
3818
|
-
|
|
4249
|
+
closePropertyPanel(true);
|
|
4250
|
+
gridPanelOpen = false; toolPanel.hidden = true;
|
|
3819
4251
|
gridBtn.setAttribute("aria-expanded", "false");
|
|
3820
4252
|
cancelRulerDrag(); colorPickerController?.abort();
|
|
3821
4253
|
return;
|
|
3822
4254
|
}
|
|
3823
4255
|
if (session.inspect && !isEditable && !e.repeat && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
|
|
3824
|
-
const button = { x: xrayBtn, r: rulersBtn, g: gridBtn, p: pipetteBtn, m: motionBtn }[e.key.toLowerCase()];
|
|
4256
|
+
const button = { x: xrayBtn, r: rulersBtn, g: gridBtn, p: pipetteBtn, t: typographyBtn, m: motionBtn }[e.key.toLowerCase()];
|
|
3825
4257
|
if (button) { e.preventDefault(); button.click(); return; }
|
|
3826
4258
|
}
|
|
3827
4259
|
|