kapi-ui 0.2.2 → 0.3.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.
@@ -0,0 +1,35 @@
1
+ export const LOGO_SVG = `
2
+ <svg class="kapi-icon kapi-logo-icon" viewBox="0 0 26 22" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M9.83829 0.291999C10.8463 -0.309151 12.1352 0.0477301 12.7171 1.08898L13.4416 2.38519C13.5585 2.59438 13.6378 2.81533 13.682 3.03966L21.7633 4.40874C24.9553 4.94957 26.8395 8.40716 25.6315 11.5069L22.8165 18.7305C22.0466 20.7059 20.1919 21.9998 18.1305 21.9998L5.05353 22C1.79996 22 -0.605067 18.8688 0.134425 15.5958L2.29225 6.04517C2.44943 5.3495 2.73555 4.71762 3.11884 4.1711L2.61288 3.26592C2.03096 2.22466 2.37628 0.893171 3.38423 0.291999C4.39221 -0.309176 5.68112 0.0477198 6.26308 1.08898L6.78925 2.03052C7.19469 1.99563 7.61032 2.01107 8.0289 2.08199L8.78478 2.21006C8.77439 1.44697 9.15304 0.700697 9.83829 0.291999ZM20.1852 14.4511C20.1114 14.0745 19.7559 13.831 19.3914 13.9072C19.0268 13.9834 18.7911 14.3507 18.8649 14.7273C19.0747 15.798 18.6868 16.6491 18.329 16.9694C18.3258 16.9723 18.3226 16.9753 18.3194 16.9782C17.9911 16.79 17.7286 16.5488 17.5759 16.3623C17.3357 16.069 16.9108 16.0324 16.6268 16.2805C16.3428 16.5286 16.3073 16.9675 16.5475 17.2609C16.8787 17.6653 17.4735 18.1798 18.2328 18.4621C19.0152 18.753 19.9859 18.7982 20.9697 18.2074C21.2911 18.0142 21.4002 17.5884 21.2134 17.2563C21.0265 16.9241 20.6142 16.8115 20.2926 17.0046C20.1342 17.0997 19.9782 17.1669 19.8258 17.2111C20.2115 16.4754 20.3891 15.491 20.1852 14.4511ZM19.454 8.45634C18.9806 7.87802 18.2281 7.73124 17.7732 8.12851C17.3183 8.52585 17.3334 9.31686 17.8068 9.89523C18.2802 10.4736 19.0327 10.6204 19.4876 10.2231C19.9424 9.82572 19.9274 9.0347 19.454 8.45634ZM23.8542 8.30534C23.4695 7.83547 22.7038 7.851 22.1439 8.33999C21.5841 8.82903 21.4421 9.60638 21.8267 10.0763C22.2114 10.5462 22.977 10.5308 23.5368 10.0418C24.0967 9.55279 24.2388 8.77526 23.8542 8.30534ZM7.60516 5.44591C7.31583 5.20445 6.8918 5.25102 6.65804 5.54986C6.42429 5.84875 6.46934 6.28678 6.75867 6.52826L8.4181 7.91313L5.8002 8.43477C5.435 8.50756 5.19603 8.87248 5.26646 9.24976C5.33693 9.62702 5.69018 9.87389 6.0554 9.80113L10.0964 8.99599C10.3537 8.94468 10.5589 8.74392 10.6223 8.48131C10.6857 8.21867 10.5959 7.94176 10.3921 7.77164L7.60516 5.44591Z" fill="white"/>
4
+ </svg>
5
+ `;
6
+ export const AI_SVG = `
7
+ <svg class="kapi-icon kapi-ai-icon" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
8
+ <path d="M15.75 13.5C16.3958 13.5 16.9688 13.9137 17.1729 14.5264L17.5674 15.709L17.5996 15.7891C17.6365 15.8678 17.6862 15.9402 17.748 16.002C17.8303 16.0842 17.9307 16.1458 18.041 16.1826L19.2236 16.5771C19.8363 16.7812 20.25 17.3542 20.25 18C20.25 18.6458 19.8363 19.2188 19.2236 19.4229L18.041 19.8174C17.9307 19.8542 17.8303 19.9158 17.748 19.998C17.6862 20.0599 17.6365 20.1322 17.5996 20.2109L17.5674 20.291L17.1729 21.4736C16.9688 22.0863 16.3958 22.5 15.75 22.5C15.1042 22.5 14.5312 22.0863 14.3271 21.4736L13.9326 20.291C13.8958 20.1807 13.8342 20.0803 13.752 19.998C13.6902 19.9362 13.6178 19.8865 13.5391 19.8496L13.459 19.8174L12.2764 19.4229C11.6637 19.2188 11.25 18.6458 11.25 18C11.25 17.3542 11.6637 16.7812 12.2764 16.5771L13.459 16.1826L13.5391 16.1504C13.6178 16.1135 13.6902 16.0638 13.752 16.002C13.8342 15.9197 13.8958 15.8193 13.9326 15.709L14.3271 14.5264L14.3691 14.4131C14.6027 13.863 15.1447 13.5 15.75 13.5ZM15.3564 16.1826C15.246 16.514 15.0595 16.8155 14.8125 17.0625L14.7178 17.1523C14.4901 17.355 14.2226 17.5098 13.9326 17.6064L12.75 18L13.9326 18.3936C14.264 18.504 14.5655 18.6905 14.8125 18.9375C15.0595 19.1845 15.246 19.486 15.3564 19.8174L15.75 21L16.1436 19.8174C16.254 19.486 16.4405 19.1845 16.6875 18.9375C16.9345 18.6905 17.236 18.504 17.5674 18.3936L18.75 18L17.5674 17.6064C17.2774 17.5098 17.0099 17.355 16.7822 17.1523L16.6875 17.0625C16.4405 16.8155 16.254 16.514 16.1436 16.1826L15.75 15L15.3564 16.1826ZM8.25 3C8.91966 3.00008 9.50842 3.444 9.69238 4.08789L10.5049 6.93359C10.645 7.42369 10.9081 7.87004 11.2686 8.23047C11.584 8.54588 11.965 8.78651 12.3838 8.93555L12.5654 8.99414L15.4121 9.80762C16.0559 9.9917 16.5 10.5804 16.5 11.25C16.4999 11.9197 16.056 12.5084 15.4121 12.6924L12.5664 13.5049C12.0763 13.645 11.63 13.9081 11.2695 14.2686C10.9091 14.629 10.6459 15.0753 10.5059 15.5654C10.5049 15.5688 10.5029 15.5718 10.502 15.5752L9.69238 18.4121C9.5084 19.0561 8.91972 19.5 8.25 19.5C7.58028 19.5 6.9916 19.0561 6.80762 18.4121L5.99512 15.5664C5.85503 15.0763 5.59187 14.63 5.23145 14.2695C4.87102 13.9091 4.42466 13.6459 3.93457 13.5059L1.08789 12.6924C0.444066 12.5083 -5.99451e-07 11.9196 0 11.25C7.77218e-05 10.5803 0.444003 9.99158 1.08789 9.80762L3.93359 8.99512C4.42369 8.85503 4.87004 8.59187 5.23047 8.23145C5.59089 7.87102 5.85406 7.42466 5.99414 6.93457L6.80762 4.08789L6.84668 3.96973C7.06535 3.39023 7.62208 3 8.25 3ZM7.43555 7.34668C7.22542 8.08181 6.83165 8.75135 6.29102 9.29199C5.75038 9.83263 5.08084 10.2264 4.3457 10.4365L1.5 11.25L4.34668 12.0645C5.08181 12.2746 5.75135 12.6683 6.29199 13.209C6.83263 13.7496 7.2264 14.4192 7.43652 15.1543L8.25 18L9.06348 15.1543L9.06445 15.1533C9.24833 14.51 9.57219 13.9164 10.0127 13.416L10.209 13.208C10.6821 12.7349 11.2537 12.374 11.8818 12.1504L12.1543 12.0635L15 11.25L12.1533 10.4355C11.51 10.2517 10.9164 9.92781 10.416 9.4873L10.208 9.29102C9.73488 8.81789 9.37395 8.24629 9.15039 7.61816L9.06348 7.3457L8.25 4.5L7.43555 7.34668ZM17.25 0C17.9381 0 18.538 0.468222 18.7051 1.13574L18.9639 2.1709C19.0463 2.50064 19.2167 2.80264 19.457 3.04297C19.6673 3.25319 19.9243 3.40984 20.2061 3.50098L20.3291 3.53613L21.3643 3.79492C22.0318 3.96203 22.5 4.56187 22.5 5.25C22.5 5.93813 22.0318 6.53797 21.3643 6.70508L20.3291 6.96387C19.9994 7.04627 19.6974 7.2167 19.457 7.45703C19.2468 7.66725 19.0902 7.92434 18.999 8.20605L18.9639 8.3291L18.7051 9.36426C18.538 10.0318 17.9381 10.5 17.25 10.5C16.5619 10.5 15.962 10.0318 15.7949 9.36426L15.5361 8.3291V8.32812C15.4537 7.99858 15.2831 7.69728 15.043 7.45703C14.8328 7.24676 14.5757 7.09019 14.2939 6.99902L14.1719 6.96387L13.1357 6.70508C12.4682 6.53798 12 5.93814 12 5.25C12 4.56186 12.4682 3.96202 13.1357 3.79492L14.1719 3.53613L14.2939 3.50098C14.5757 3.40981 14.8328 3.25324 15.043 3.04297C15.2831 2.80272 15.4537 2.50142 15.5361 2.17188V2.1709L15.7949 1.13574L15.8311 1.0127C16.0372 0.411345 16.605 0 17.25 0ZM16.9912 2.53516C16.843 3.12845 16.5358 3.67 16.1035 4.10254C15.6711 4.53511 15.1295 4.84271 14.5361 4.99121L13.5 5.25L14.5361 5.50879C15.1295 5.65729 15.6711 5.96489 16.1035 6.39746C16.5358 6.83 16.843 7.37155 16.9912 7.96484L17.25 9L17.5088 7.96484C17.6571 7.37135 17.9639 6.82906 18.3965 6.39648C18.8291 5.96391 19.3713 5.65713 19.9648 5.50879L21 5.25L19.9648 4.99121C19.4457 4.86145 18.966 4.61015 18.5645 4.25977L18.3965 4.10352C17.9639 3.67094 17.6571 3.12865 17.5088 2.53516L17.25 1.5L16.9912 2.53516Z" fill="white"/>
9
+ </svg>
10
+ `;
11
+ export const COPY_SVG = `
12
+ <svg class="kapi-icon kapi-copy-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
13
+ <rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
14
+ <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
15
+ </svg>
16
+ `;
17
+ export const CHECK_SVG = `
18
+ <svg class="kapi-icon kapi-check-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
19
+ <circle cx="12" cy="12" r="10"/>
20
+ <path d="m9 12 2 2 4-4"/>
21
+ </svg>
22
+ `;
23
+ export const DELETE_SVG = `
24
+ <svg class="kapi-icon kapi-delete-icon" viewBox="0 0 18 21" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
25
+ <path d="M12.0004 2.97734C12.0004 2.18252 11.3925 1.54998 10.6371 1.52617H10.6361C9.54593 1.49133 8.45489 1.49133 7.36466 1.52617H7.36369C6.60853 1.54997 6.00041 2.1833 6.00041 2.97734V3.08964C7.99855 2.96812 10.0023 2.96812 12.0004 3.08964V2.97734ZM12.6928 4.6414C10.235 4.45145 7.76583 4.45145 5.30802 4.6414C4.39387 4.71218 3.48206 4.80969 2.57365 4.93339L3.5883 18.116C3.61744 18.4927 3.78759 18.8447 4.06486 19.1014C4.34221 19.358 4.70651 19.5009 5.08439 19.5008H12.9164L13.058 19.4939C13.3845 19.4631 13.6934 19.3258 13.936 19.1014C14.2132 18.8447 14.3834 18.4927 14.4125 18.116L15.4262 4.93339C14.5181 4.80975 13.6066 4.71215 12.6928 4.6414ZM13.5004 3.20488C14.4501 3.292 15.3972 3.40606 16.3402 3.54863H16.3412C16.6878 3.60132 17.0328 3.65762 17.3783 3.71757C17.7864 3.78839 18.0605 4.17665 17.9897 4.58476C17.9188 4.9927 17.5304 5.26576 17.1225 5.19511C17.0527 5.183 16.9823 5.17271 16.9125 5.16093L15.9086 18.2312C15.8504 18.9848 15.5092 19.6886 14.9545 20.2019C14.3999 20.7152 13.6721 21.0009 12.9164 21.0008H5.08439C4.32869 21.0009 3.60095 20.7152 3.0463 20.2019C2.49163 19.6886 2.15037 18.9848 2.0922 18.2312L1.08732 5.15996C1.01783 5.17167 0.947825 5.18208 0.878335 5.19414C0.470377 5.26478 0.0820323 4.99171 0.0111474 4.58378C-0.0596724 4.17567 0.214361 3.78742 0.622476 3.7166C0.967631 3.6567 1.31296 3.60047 1.66056 3.54863L2.54044 3.42363C3.19218 3.3372 3.8457 3.26494 4.50041 3.20488V2.97734C4.50041 1.41349 5.71213 0.0775151 7.31681 0.0271432L8.15861 0.00663535C9.00041 -0.00680924 9.84244 0.000251256 10.684 0.0271432L10.8334 0.0349557C12.3631 0.160609 13.5004 1.46122 13.5004 2.97734V3.20488Z" fill="currentColor"/>
26
+ </svg>
27
+ `;
28
+ // viewBox is cropped to the path's actual ink bounding box (0,0 to ~4.08,4.08),
29
+ // not the original 0 0 5 5 -- the artwork isn't centered in that stated box,
30
+ // so a naive flex-centered render looks visibly off.
31
+ export const ARROW_SVG = `<svg viewBox="0 0 4.08228 4.08228" width="10" height="10" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M4.08228 1.9241L3.65301 2.33273L2.35468 1.10883L2.35468 4.08228L1.72761 4.08228L1.72761 1.10883L0.431375 2.33273L-9.43368e-08 1.9241L2.04114 -6.76765e-06L4.08228 1.9241Z" fill="#1E1E1F"/></svg>`;
32
+ export const STOP_SVG = `<svg class="kapi-icon kapi-stop-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="white" aria-hidden="true">
33
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
34
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 9.563C9 9.252 9.252 9 9.563 9h4.874c.311 0 .563.252.563.563v4.874c0 .311-.252.563-.563.563H9.564A.562.562 0 0 1 9 14.437V9.564Z" />
35
+ </svg>`;
@@ -0,0 +1,423 @@
1
+ import { findTraceFromElement } from './trace-record.js';
2
+ const HIGHLIGHT_COLOR = '34, 197, 94'; // green-500, as an rgb triplet for reuse in rgba()
3
+ const SELECTION_COLOR = '34, 197, 94'; // green-500, matches single-select highlight; for multi-select outlines/marquee
4
+ const IGNORE_SELECTOR = 'kapi-overlay, kapi-hover-panel, kapi-comments';
5
+ const DRAG_THRESHOLD = 4;
6
+ const BOX_SELECT_STEP = 16; // px between elementsFromPoint samples inside a drag box
7
+ let highlightEl = null;
8
+ let blockerEl = null;
9
+ let hoveredEl = null;
10
+ let active = false;
11
+ let locked = false;
12
+ let disabled = false;
13
+ let onHover = null;
14
+ let onElementClick = null;
15
+ // Multi-select state: shift-click toggles individual elements, drag draws a
16
+ // marquee and adds every element sampled inside it. Both feed the same Set;
17
+ // every change is pushed out immediately so the caller can keep a composer
18
+ // live while the selection grows. A plain click clears it.
19
+ const selected = new Set();
20
+ let onSelectionChange = null;
21
+ // Blue outline boxes, shared by two callers: the live selection above, and
22
+ // beginEdit() in comments.ts previewing an existing multi-target comment's
23
+ // elements. Whichever last wrote `outlineTargets` wins the paint.
24
+ let outlineTargets = [];
25
+ let selectionOutlineEls = [];
26
+ let marqueeEl = null;
27
+ let boxDragStart = null;
28
+ let boxDragging = false;
29
+ let justBoxSelected = false; // swallows the click that follows a drag-release
30
+ function ensureMarqueeEl() {
31
+ if (marqueeEl)
32
+ return marqueeEl;
33
+ const el = document.createElement('div');
34
+ el.style.cssText = `
35
+ position: fixed;
36
+ pointer-events: none;
37
+ box-sizing: border-box;
38
+ background: rgba(${SELECTION_COLOR}, 0.1);
39
+ border: 1px solid rgb(${SELECTION_COLOR});
40
+ z-index: 2147483646;
41
+ display: none;
42
+ `;
43
+ document.body.appendChild(el);
44
+ marqueeEl = el;
45
+ return el;
46
+ }
47
+ function paintMarquee(x1, y1, x2, y2) {
48
+ const box = ensureMarqueeEl();
49
+ box.style.display = 'block';
50
+ box.style.left = `${Math.min(x1, x2)}px`;
51
+ box.style.top = `${Math.min(y1, y2)}px`;
52
+ box.style.width = `${Math.abs(x2 - x1)}px`;
53
+ box.style.height = `${Math.abs(y2 - y1)}px`;
54
+ }
55
+ function hideMarquee() {
56
+ if (marqueeEl)
57
+ marqueeEl.style.display = 'none';
58
+ }
59
+ function paintOutlines() {
60
+ selectionOutlineEls.forEach((el) => el.remove());
61
+ selectionOutlineEls = outlineTargets.map((el) => {
62
+ const box = document.createElement('div');
63
+ const rect = el.getBoundingClientRect();
64
+ box.style.cssText = `
65
+ position: fixed;
66
+ top: 0;
67
+ left: 0;
68
+ pointer-events: none;
69
+ box-sizing: border-box;
70
+ background: rgba(${SELECTION_COLOR}, 0.15);
71
+ border: 1px solid rgb(${SELECTION_COLOR});
72
+ z-index: 2147483646;
73
+ transform: translate(${rect.left}px, ${rect.top}px);
74
+ width: ${rect.width}px;
75
+ height: ${rect.height}px;
76
+ `;
77
+ document.body.appendChild(box);
78
+ return box;
79
+ });
80
+ }
81
+ function repositionOutlines() {
82
+ if (outlineTargets.length > 0)
83
+ paintOutlines();
84
+ }
85
+ window.addEventListener('resize', repositionOutlines);
86
+ window.addEventListener('scroll', repositionOutlines, true);
87
+ export function setOnSelectionChange(callback) {
88
+ onSelectionChange = callback;
89
+ }
90
+ function notifySelectionChange() {
91
+ outlineTargets = [...selected];
92
+ paintOutlines();
93
+ onSelectionChange?.([...selected]);
94
+ }
95
+ // Shows blue outlines on an arbitrary set of elements without touching the
96
+ // interactive `selected` Set above — used to preview an existing multi-target
97
+ // comment's elements while it's being edited.
98
+ export function previewElements(els) {
99
+ outlineTargets = els;
100
+ paintOutlines();
101
+ }
102
+ export function clearPreview() {
103
+ outlineTargets = [];
104
+ paintOutlines();
105
+ }
106
+ function toggleSelected(el) {
107
+ selected.has(el) ? selected.delete(el) : selected.add(el);
108
+ notifySelectionChange();
109
+ }
110
+ export function clearSelection() {
111
+ if (selected.size === 0)
112
+ return;
113
+ selected.clear();
114
+ notifySelectionChange();
115
+ }
116
+ function elementsInBox(x1, y1, x2, y2) {
117
+ const left = Math.min(x1, x2);
118
+ const right = Math.max(x1, x2);
119
+ const top = Math.min(y1, y2);
120
+ const bottom = Math.max(y1, y2);
121
+ const found = new Set();
122
+ for (let y = top; y <= bottom; y += BOX_SELECT_STEP) {
123
+ for (let x = left; x <= right; x += BOX_SELECT_STEP) {
124
+ const stack = document.elementsFromPoint(x, y);
125
+ if (stack[0]?.closest(IGNORE_SELECTOR))
126
+ continue;
127
+ const el = stack.find(isInspectable);
128
+ if (el)
129
+ found.add(el);
130
+ }
131
+ }
132
+ return [...found];
133
+ }
134
+ function selectElementsInBox(x1, y1, x2, y2, additive) {
135
+ if (!additive)
136
+ selected.clear();
137
+ for (const el of elementsInBox(x1, y1, x2, y2))
138
+ selected.add(el);
139
+ notifySelectionChange();
140
+ }
141
+ function handleBlockerPointerDown(e) {
142
+ if (e.button !== 0)
143
+ return;
144
+ boxDragStart = { x: e.clientX, y: e.clientY };
145
+ boxDragging = false;
146
+ // Leave the hover highlight up during the press: a plain click keeps it lit
147
+ // straight through to lockHighlightOn (no flicker), and a real drag-select
148
+ // clears it once the drag threshold is crossed (see handleBlockerPointerMove).
149
+ blockerEl?.setPointerCapture(e.pointerId);
150
+ }
151
+ function handleBlockerPointerMove(e) {
152
+ if (!boxDragStart)
153
+ return;
154
+ const dx = e.clientX - boxDragStart.x;
155
+ const dy = e.clientY - boxDragStart.y;
156
+ if (!boxDragging) {
157
+ if (Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD)
158
+ return;
159
+ boxDragging = true;
160
+ clearHighlight(); // hide the hover box so it doesn't fight the marquee
161
+ }
162
+ paintMarquee(boxDragStart.x, boxDragStart.y, e.clientX, e.clientY);
163
+ // Live-preview every element the box currently touches (union with the
164
+ // existing selection when additive), so the user sees what they're grabbing
165
+ // before releasing. Committed to `selected` only on pointer-up.
166
+ const touched = elementsInBox(boxDragStart.x, boxDragStart.y, e.clientX, e.clientY);
167
+ previewElements(e.shiftKey ? [...new Set([...selected, ...touched])] : touched);
168
+ }
169
+ function handleBlockerPointerUp(e) {
170
+ if (!boxDragStart)
171
+ return;
172
+ if (boxDragging) {
173
+ selectElementsInBox(boxDragStart.x, boxDragStart.y, e.clientX, e.clientY, e.shiftKey);
174
+ hideMarquee();
175
+ justBoxSelected = true;
176
+ }
177
+ boxDragStart = null;
178
+ boxDragging = false;
179
+ }
180
+ function ensureHighlightEl() {
181
+ if (highlightEl)
182
+ return highlightEl;
183
+ const el = document.createElement('div');
184
+ el.style.cssText = `
185
+ position: fixed;
186
+ top: 0;
187
+ left: 0;
188
+ pointer-events: none;
189
+ box-sizing: border-box;
190
+ background: rgba(${HIGHLIGHT_COLOR}, 0.2);
191
+ border: 1px solid rgb(${HIGHLIGHT_COLOR});
192
+ z-index: 2147483646;
193
+ display: none;
194
+ transition: transform 120ms ease, width 120ms ease, height 120ms ease;
195
+ `;
196
+ document.body.appendChild(el);
197
+ highlightEl = el;
198
+ return el;
199
+ }
200
+ function handleBlockerClick(e) {
201
+ if (justBoxSelected) {
202
+ justBoxSelected = false;
203
+ return; // this click is the tail end of a drag-select release, not a real click
204
+ }
205
+ const stack = document.elementsFromPoint(e.clientX, e.clientY);
206
+ if (stack[0]?.closest(IGNORE_SELECTOR))
207
+ return;
208
+ const el = stack.find(isInspectable) ?? null;
209
+ if (!el)
210
+ return;
211
+ if (e.shiftKey) {
212
+ toggleSelected(el);
213
+ return;
214
+ }
215
+ if (selected.size > 0) {
216
+ clearSelection();
217
+ return;
218
+ }
219
+ onElementClick?.(el, e.clientX, e.clientY);
220
+ }
221
+ export function isBoxSelectClick() {
222
+ return justBoxSelected;
223
+ }
224
+ function ensureBlockerEl() {
225
+ if (blockerEl)
226
+ return blockerEl;
227
+ const el = document.createElement('div');
228
+ el.style.cssText = `
229
+ position: fixed;
230
+ inset: 0;
231
+ pointer-events: auto;
232
+ background: transparent;
233
+ z-index: 2147483645;
234
+ display: none;
235
+ `;
236
+ el.addEventListener('click', handleBlockerClick);
237
+ el.addEventListener('pointerdown', handleBlockerPointerDown);
238
+ el.addEventListener('pointermove', handleBlockerPointerMove);
239
+ el.addEventListener('pointerup', handleBlockerPointerUp);
240
+ document.body.appendChild(el);
241
+ blockerEl = el;
242
+ return el;
243
+ }
244
+ function isInspectable(el) {
245
+ if (!el)
246
+ return false;
247
+ if (el === blockerEl)
248
+ return false;
249
+ if (el.closest(IGNORE_SELECTOR))
250
+ return false;
251
+ return true;
252
+ }
253
+ function buildSelectorPath(el) {
254
+ const parts = [];
255
+ let node = el;
256
+ while (node && node !== document.body && node !== document.documentElement) {
257
+ let part = node.tagName.toLowerCase();
258
+ if (node.id)
259
+ part += `#${node.id}`;
260
+ else if (node.classList.length > 0)
261
+ part += `.${node.classList[0]}`;
262
+ parts.unshift(part);
263
+ node = node.parentElement;
264
+ }
265
+ return parts.join(' > ') || el.tagName.toLowerCase();
266
+ }
267
+ // Most elements resolve directly via `el.__vnode` (see trace-record.ts). The
268
+ // ancestor walk below exists for the elements that can't: plain text nodes
269
+ // (no vnode at all), `_createStaticVNode` content (raw innerHTML — Vue never
270
+ // creates individual vnodes for its inner elements), and vnodes Vue cloned
271
+ // *with* extra props (`cloneVNode(vnode, extraProps)` builds a fresh `props`
272
+ // object via `mergeProps`, breaking the WeakMap identity link — a clone with
273
+ // no extraProps reuses the same `props` reference and resolves directly).
274
+ // In all of these, the nearest traced ancestor is the best available
275
+ // approximation.
276
+ export function getSourceLocation(el) {
277
+ let node = el;
278
+ while (node) {
279
+ const trace = findTraceFromElement(node);
280
+ if (trace)
281
+ return trace;
282
+ node = node.parentElement;
283
+ }
284
+ return null;
285
+ }
286
+ let warnedMissingComponentInfo = false;
287
+ export function getComponentInfo(el) {
288
+ const instance = el.__vueParentComponent;
289
+ if (!instance) {
290
+ // `el.__vnode` (see trace-record.ts) is a separate internal Vue
291
+ // back-reference from `__vueParentComponent`. If it's present but
292
+ // `__vueParentComponent` isn't, that's not "this element isn't
293
+ // Vue-managed" (the normal, silent null case) — it's a sign Vue
294
+ // renamed/removed this internal property, so warn once rather than
295
+ // degrading silently everywhere.
296
+ if (!warnedMissingComponentInfo && findTraceFromElement(el)) {
297
+ warnedMissingComponentInfo = true;
298
+ console.warn('[kapi] Could not resolve Vue component info for an element with a known source location. ' +
299
+ "Vue's internal `__vueParentComponent` property may have changed in this Vue version — " +
300
+ 'component names/files will be unavailable until kapi is updated to match.');
301
+ }
302
+ return null;
303
+ }
304
+ const name = instance.type.name || instance.type.__name;
305
+ if (!name)
306
+ return null;
307
+ return { name, file: instance.type.__file ?? null };
308
+ }
309
+ // Shared by the hover panel and comment tooltips, which both render a
310
+ // `<ComponentName>` badge but style it with their own class.
311
+ export function renderComponentBadge(component, className) {
312
+ const el = document.createElement('div');
313
+ el.className = className;
314
+ el.textContent = `<${component.name}>`;
315
+ return el;
316
+ }
317
+ export function describeElement(el) {
318
+ return {
319
+ tag: el.tagName.toLowerCase(),
320
+ id: el.id || null,
321
+ classes: [...el.classList],
322
+ selector: buildSelectorPath(el),
323
+ source: getSourceLocation(el),
324
+ component: getComponentInfo(el),
325
+ };
326
+ }
327
+ function paintHighlight(el) {
328
+ const box = ensureHighlightEl();
329
+ const rect = el.getBoundingClientRect();
330
+ box.style.display = 'block';
331
+ box.style.transform = `translate(${rect.left}px, ${rect.top}px)`;
332
+ box.style.width = `${rect.width}px`;
333
+ box.style.height = `${rect.height}px`;
334
+ }
335
+ function clearHighlight() {
336
+ if (highlightEl)
337
+ highlightEl.style.display = 'none';
338
+ hoveredEl = null;
339
+ onHover?.(null);
340
+ }
341
+ function handlePointerMove(e) {
342
+ // Freeze hover once the pointer is down on the blocker (boxDragStart set):
343
+ // the cursor is committing to a click or a drag-select, not hovering. This
344
+ // also wins the capture-phase race on the drag's first move, where this
345
+ // handler would otherwise repaint the hover box a beat before the blocker's
346
+ // handler clears it.
347
+ if (locked || boxDragging || boxDragStart)
348
+ return;
349
+ const stack = document.elementsFromPoint(e.clientX, e.clientY);
350
+ // The overlay/hover-panel/comments UI sits above the blocker in z-index, so
351
+ // when the cursor is directly over it, it's the topmost hit. Bail out here
352
+ // rather than falling through to stack.find, which would otherwise select
353
+ // whatever page element happens to sit behind our own UI.
354
+ if (stack[0]?.closest(IGNORE_SELECTOR)) {
355
+ clearHighlight();
356
+ return;
357
+ }
358
+ const el = stack.find(isInspectable) ?? null;
359
+ if (!el) {
360
+ clearHighlight();
361
+ return;
362
+ }
363
+ if (el === hoveredEl)
364
+ return;
365
+ hoveredEl = el;
366
+ paintHighlight(el);
367
+ onHover?.(el);
368
+ }
369
+ export function setOnHover(callback) {
370
+ onHover = callback;
371
+ }
372
+ export function lockHighlightOn(el) {
373
+ locked = true;
374
+ hoveredEl = el;
375
+ paintHighlight(el);
376
+ onHover?.(null);
377
+ }
378
+ // Freezes hover (like lockHighlightOn) but skips the single green highlight
379
+ // box — used while composing on a multi-select batch, where the blue
380
+ // selection outlines (see notifySelectionChange) already mark every element
381
+ // and a green box on just the anchor would visually clash with them.
382
+ export function lockWithoutHighlight() {
383
+ locked = true;
384
+ clearHighlight();
385
+ }
386
+ export function unlockHighlight() {
387
+ locked = false;
388
+ }
389
+ export function setOnElementClick(callback) {
390
+ onElementClick = callback;
391
+ }
392
+ export function startInspecting() {
393
+ if (active || disabled)
394
+ return;
395
+ active = true;
396
+ document.addEventListener('pointermove', handlePointerMove, true);
397
+ ensureBlockerEl().style.display = 'block';
398
+ }
399
+ export function stopInspecting() {
400
+ if (!active)
401
+ return;
402
+ active = false;
403
+ document.removeEventListener('pointermove', handlePointerMove, true);
404
+ clearHighlight();
405
+ clearSelection();
406
+ if (blockerEl)
407
+ blockerEl.style.display = 'none';
408
+ }
409
+ // Forcibly disables inspecting regardless of the bar's expand/collapse state,
410
+ // and blocks it from being re-enabled until re-allowed (e.g. while Claude is
411
+ // processing submitted comments).
412
+ export function setDisabled(value) {
413
+ disabled = value;
414
+ if (disabled)
415
+ stopInspecting();
416
+ }
417
+ export function isDisabled() {
418
+ return disabled;
419
+ }
420
+ export function clearHighlightIfNotInspecting() {
421
+ if (!active)
422
+ clearHighlight();
423
+ }