kapi-ui 0.2.1 → 0.3.0
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/LICENSE +21 -0
- package/dist/browser/comments.js +531 -0
- package/dist/{hover-panel.js → browser/hover-panel.js} +63 -17
- package/dist/browser/icons.js +35 -0
- package/dist/browser/inspector.js +423 -0
- package/dist/browser/overlay.js +322 -0
- package/dist/browser/socket.js +38 -0
- package/dist/browser/styles/comments.css +246 -0
- package/dist/browser/styles/overlay.css +181 -0
- package/dist/browser/trace-record.js +33 -0
- package/dist/browser/types.js +4 -0
- package/dist/cli/setup.js +190 -0
- package/dist/cli/types.js +2 -0
- package/dist/{utils.js → cli/utils.js} +39 -11
- package/dist/node/claude-agent.js +185 -0
- package/dist/node/codex-agent.js +138 -0
- package/dist/node/nuxt-module.js +34 -0
- package/dist/node/types.js +4 -0
- package/dist/node/vite-plugin.js +213 -0
- package/package.json +12 -11
- package/dist/comments.js +0 -363
- package/dist/constants.js +0 -1
- package/dist/inspector.js +0 -168
- package/dist/location-transform.js +0 -47
- package/dist/nuxt-module.js +0 -21
- package/dist/overlay.js +0 -378
- package/dist/server.js +0 -118
- package/dist/setup.js +0 -65
- package/dist/socket.js +0 -36
- package/dist/types.js +0 -1
- package/dist/vite-plugin.js +0 -44
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { startInspecting, stopInspecting, setOnHover, setOnElementClick, setOnSelectionChange, setDisabled } from './inspector.js';
|
|
2
|
+
import { updateHoverPanel, showProcessingStatus } from './hover-panel.js';
|
|
3
|
+
import { beginComment, updateSelection, clearAllComments, cancelOpenDraft, buildAllCommentsPrompt } from './comments.js';
|
|
4
|
+
import { connectSocket, sendComments, stopComments, setOnCommentsDone, setOnCommentsError, setOnCommentsProcessing } from './socket.js';
|
|
5
|
+
import { LOGO_SVG, AI_SVG, COPY_SVG, CHECK_SVG, DELETE_SVG, STOP_SVG } from './icons.js';
|
|
6
|
+
import styles from './styles/overlay.css?inline';
|
|
7
|
+
const KAPI_TAG = 'kapi-overlay';
|
|
8
|
+
const POSITION_KEY = 'kapi-overlay-position';
|
|
9
|
+
const DRAG_THRESHOLD = 4;
|
|
10
|
+
const COLLAPSED_WIDTH = 40;
|
|
11
|
+
const BAR_HEIGHT = 40;
|
|
12
|
+
const INSET = 20;
|
|
13
|
+
function loadPosition() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = localStorage.getItem(POSITION_KEY);
|
|
16
|
+
if (!raw)
|
|
17
|
+
return null;
|
|
18
|
+
const parsed = JSON.parse(raw);
|
|
19
|
+
if (typeof parsed?.left === 'number' && typeof parsed?.top === 'number')
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* ignore corrupt/inaccessible storage */
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function savePosition(position) {
|
|
28
|
+
try {
|
|
29
|
+
localStorage.setItem(POSITION_KEY, JSON.stringify(position));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* ignore (e.g. storage disabled) */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function clamp(value, min, max) {
|
|
36
|
+
return Math.min(Math.max(value, min), max);
|
|
37
|
+
}
|
|
38
|
+
/** Creates and wires up the floating Kapi overlay. */
|
|
39
|
+
export function insertOverlay() {
|
|
40
|
+
// 1. Only one overlay may exist on a page.
|
|
41
|
+
if (document.querySelector(KAPI_TAG))
|
|
42
|
+
return;
|
|
43
|
+
connectSocket();
|
|
44
|
+
// 2. Build the Shadow DOM UI. Its styles stay isolated from the host page.
|
|
45
|
+
const host = document.createElement(KAPI_TAG);
|
|
46
|
+
const root = host.attachShadow({ mode: 'open' });
|
|
47
|
+
const style = document.createElement('style');
|
|
48
|
+
style.textContent = styles;
|
|
49
|
+
const bar = document.createElement('div');
|
|
50
|
+
bar.className = 'kapi-bar';
|
|
51
|
+
const logoBtn = document.createElement('button');
|
|
52
|
+
logoBtn.className = 'kapi-btn kapi-logo-btn';
|
|
53
|
+
logoBtn.type = 'button';
|
|
54
|
+
logoBtn.setAttribute('aria-label', 'Toggle Kapi');
|
|
55
|
+
logoBtn.setAttribute('aria-expanded', 'false');
|
|
56
|
+
logoBtn.innerHTML = LOGO_SVG;
|
|
57
|
+
const extra = document.createElement('div');
|
|
58
|
+
extra.className = 'kapi-extra';
|
|
59
|
+
const makeDivider = () => {
|
|
60
|
+
const divider = document.createElement('span');
|
|
61
|
+
divider.className = 'kapi-divider';
|
|
62
|
+
return divider;
|
|
63
|
+
};
|
|
64
|
+
// The vite plugin / nuxt module injects this global only when the agent
|
|
65
|
+
// session is disabled (`agent: false`); default (undefined) means enabled.
|
|
66
|
+
const agentEnabled = window.__KAPI_AGENT_ENABLED__ !== false;
|
|
67
|
+
const aiBtn = document.createElement('button');
|
|
68
|
+
aiBtn.className = 'kapi-btn';
|
|
69
|
+
aiBtn.type = 'button';
|
|
70
|
+
aiBtn.setAttribute('aria-label', 'AI');
|
|
71
|
+
aiBtn.innerHTML = AI_SVG;
|
|
72
|
+
aiBtn.addEventListener('click', () => {
|
|
73
|
+
const prompt = buildAllCommentsPrompt();
|
|
74
|
+
if (prompt)
|
|
75
|
+
sendComments(prompt);
|
|
76
|
+
});
|
|
77
|
+
const copyBtn = document.createElement('button');
|
|
78
|
+
copyBtn.className = 'kapi-btn';
|
|
79
|
+
copyBtn.type = 'button';
|
|
80
|
+
copyBtn.setAttribute('aria-label', 'Copy');
|
|
81
|
+
copyBtn.innerHTML = COPY_SVG;
|
|
82
|
+
// Swap the button icon (copy <-> checkmark) with a fade/scale/blur out-then-in.
|
|
83
|
+
const swapCopyIcon = (svg, done) => {
|
|
84
|
+
copyBtn.classList.remove('kapi-copy-in');
|
|
85
|
+
copyBtn.classList.add('kapi-copy-out');
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
copyBtn.innerHTML = svg;
|
|
88
|
+
copyBtn.classList.replace('kapi-copy-out', 'kapi-copy-in');
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
copyBtn.classList.remove('kapi-copy-in');
|
|
91
|
+
done?.();
|
|
92
|
+
}, 180);
|
|
93
|
+
}, 180);
|
|
94
|
+
};
|
|
95
|
+
copyBtn.addEventListener('click', async () => {
|
|
96
|
+
const prompt = buildAllCommentsPrompt();
|
|
97
|
+
if (!prompt)
|
|
98
|
+
return;
|
|
99
|
+
try {
|
|
100
|
+
await navigator.clipboard.writeText(prompt);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return; // clipboard blocked; no success feedback
|
|
104
|
+
}
|
|
105
|
+
copyBtn.classList.add('kapi-busy'); // clicks disabled until we're back to the copy icon
|
|
106
|
+
swapCopyIcon(CHECK_SVG, () => setTimeout(() => swapCopyIcon(COPY_SVG, () => copyBtn.classList.remove('kapi-busy')), 1000));
|
|
107
|
+
});
|
|
108
|
+
const deleteBtn = document.createElement('button');
|
|
109
|
+
deleteBtn.className = 'kapi-btn';
|
|
110
|
+
deleteBtn.type = 'button';
|
|
111
|
+
deleteBtn.setAttribute('aria-label', 'Delete');
|
|
112
|
+
deleteBtn.innerHTML = DELETE_SVG;
|
|
113
|
+
deleteBtn.addEventListener('click', () => clearAllComments());
|
|
114
|
+
const btnGroup = document.createElement('div');
|
|
115
|
+
btnGroup.className = 'kapi-btn-group';
|
|
116
|
+
// Manual mode (agent disabled) drops the AI "send" button — Copy/Delete stay.
|
|
117
|
+
btnGroup.append(...(agentEnabled ? [aiBtn] : []), copyBtn, deleteBtn);
|
|
118
|
+
extra.append(makeDivider(), btnGroup);
|
|
119
|
+
bar.append(logoBtn, extra);
|
|
120
|
+
root.append(style, bar);
|
|
121
|
+
document.body.appendChild(host);
|
|
122
|
+
// 3. Connect the overlay to the element inspector.
|
|
123
|
+
setOnHover((el) => {
|
|
124
|
+
updateHoverPanel(el);
|
|
125
|
+
});
|
|
126
|
+
setOnElementClick((el, clientX, clientY) => {
|
|
127
|
+
beginComment(el, clientX, clientY);
|
|
128
|
+
});
|
|
129
|
+
setOnSelectionChange((els) => {
|
|
130
|
+
updateSelection(els);
|
|
131
|
+
});
|
|
132
|
+
// 4. Track comment-processing state and control the logo button.
|
|
133
|
+
let isProcessing = false;
|
|
134
|
+
let wasStopped = false;
|
|
135
|
+
const handleLogoClick = () => setExpanded(!expanded);
|
|
136
|
+
const handleStopClick = () => {
|
|
137
|
+
wasStopped = true;
|
|
138
|
+
stopComments();
|
|
139
|
+
};
|
|
140
|
+
let logoSwapTimeout = null;
|
|
141
|
+
let logoSwapInnerTimeout = null;
|
|
142
|
+
const clearPendingLogoSwap = () => {
|
|
143
|
+
if (logoSwapTimeout !== null)
|
|
144
|
+
clearTimeout(logoSwapTimeout);
|
|
145
|
+
if (logoSwapInnerTimeout !== null)
|
|
146
|
+
clearTimeout(logoSwapInnerTimeout);
|
|
147
|
+
logoSwapTimeout = null;
|
|
148
|
+
logoSwapInnerTimeout = null;
|
|
149
|
+
};
|
|
150
|
+
const swapLogo = (svg) => {
|
|
151
|
+
clearPendingLogoSwap();
|
|
152
|
+
logoBtn.classList.remove('kapi-animating-in');
|
|
153
|
+
logoBtn.classList.add('kapi-animating-out');
|
|
154
|
+
logoSwapTimeout = setTimeout(() => {
|
|
155
|
+
logoBtn.innerHTML = svg;
|
|
156
|
+
logoBtn.classList.remove('kapi-animating-out');
|
|
157
|
+
logoBtn.classList.add('kapi-animating-in');
|
|
158
|
+
logoSwapInnerTimeout = setTimeout(() => {
|
|
159
|
+
logoBtn.classList.remove('kapi-animating-in');
|
|
160
|
+
logoSwapInnerTimeout = null;
|
|
161
|
+
}, 200);
|
|
162
|
+
logoSwapTimeout = null;
|
|
163
|
+
}, 200);
|
|
164
|
+
};
|
|
165
|
+
const swapLogoToStop = () => {
|
|
166
|
+
swapLogo(STOP_SVG);
|
|
167
|
+
logoBtn.classList.add('kapi-stop-mode');
|
|
168
|
+
logoBtn.removeEventListener('click', handleLogoClick);
|
|
169
|
+
logoBtn.addEventListener('click', handleStopClick);
|
|
170
|
+
};
|
|
171
|
+
const restoreLogoFromStop = () => {
|
|
172
|
+
swapLogo(LOGO_SVG);
|
|
173
|
+
logoBtn.classList.remove('kapi-stop-mode');
|
|
174
|
+
logoBtn.removeEventListener('click', handleStopClick);
|
|
175
|
+
logoBtn.addEventListener('click', handleLogoClick);
|
|
176
|
+
};
|
|
177
|
+
const finishProcessing = (clearComments) => {
|
|
178
|
+
isProcessing = false;
|
|
179
|
+
restoreLogoFromStop();
|
|
180
|
+
if (clearComments && !wasStopped)
|
|
181
|
+
clearAllComments();
|
|
182
|
+
setDisabled(false);
|
|
183
|
+
setExpanded(true);
|
|
184
|
+
updateHoverPanel(null);
|
|
185
|
+
};
|
|
186
|
+
// 5. Reflect comment-processing lifecycle events in the UI.
|
|
187
|
+
setOnCommentsProcessing((status) => {
|
|
188
|
+
if (isProcessing) {
|
|
189
|
+
showProcessingStatus(status);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
cancelOpenDraft();
|
|
193
|
+
setDisabled(true);
|
|
194
|
+
isProcessing = true;
|
|
195
|
+
wasStopped = false;
|
|
196
|
+
setExpanded(false);
|
|
197
|
+
swapLogoToStop();
|
|
198
|
+
showProcessingStatus(status);
|
|
199
|
+
});
|
|
200
|
+
setOnCommentsDone(() => {
|
|
201
|
+
finishProcessing(true);
|
|
202
|
+
});
|
|
203
|
+
setOnCommentsError((message) => {
|
|
204
|
+
finishProcessing(false);
|
|
205
|
+
showProcessingStatus(`Error: ${message}`);
|
|
206
|
+
});
|
|
207
|
+
// 6. Expand/collapse the toolbar and keep it on screen.
|
|
208
|
+
let expanded = false;
|
|
209
|
+
const measureExpandedWidth = () => {
|
|
210
|
+
const barStyle = getComputedStyle(bar);
|
|
211
|
+
const paddingX = parseFloat(barStyle.paddingLeft) + parseFloat(barStyle.paddingRight);
|
|
212
|
+
const gap = parseFloat(barStyle.columnGap || barStyle.gap) || 0;
|
|
213
|
+
const width = Math.round(logoBtn.offsetWidth + gap + extra.offsetWidth + paddingX);
|
|
214
|
+
bar.style.setProperty('--kapi-expanded-width', `${width}px`);
|
|
215
|
+
return width;
|
|
216
|
+
};
|
|
217
|
+
let expandedWidth = measureExpandedWidth();
|
|
218
|
+
const currentSize = () => ({
|
|
219
|
+
width: expanded ? expandedWidth : COLLAPSED_WIDTH,
|
|
220
|
+
height: BAR_HEIGHT,
|
|
221
|
+
});
|
|
222
|
+
const place = (left, top) => {
|
|
223
|
+
const { width, height } = currentSize();
|
|
224
|
+
const maxLeft = window.innerWidth - width - INSET;
|
|
225
|
+
const maxTop = window.innerHeight - height - INSET;
|
|
226
|
+
host.style.left = `${clamp(left, INSET, Math.max(INSET, maxLeft))}px`;
|
|
227
|
+
host.style.top = `${clamp(top, INSET, Math.max(INSET, maxTop))}px`;
|
|
228
|
+
};
|
|
229
|
+
const setExpanded = (next) => {
|
|
230
|
+
if (expanded === next)
|
|
231
|
+
return;
|
|
232
|
+
if (next)
|
|
233
|
+
expandedWidth = measureExpandedWidth();
|
|
234
|
+
expanded = next;
|
|
235
|
+
bar.classList.toggle('kapi-expanded', expanded);
|
|
236
|
+
logoBtn.setAttribute('aria-expanded', String(expanded));
|
|
237
|
+
const rect = host.getBoundingClientRect();
|
|
238
|
+
place(rect.left, rect.top);
|
|
239
|
+
if (expanded) {
|
|
240
|
+
startInspecting();
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
stopInspecting();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
// 7. Restore the saved position, then keep it valid after viewport changes.
|
|
247
|
+
const saved = loadPosition();
|
|
248
|
+
if (saved) {
|
|
249
|
+
place(saved.left, saved.top);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
place(INSET, window.innerHeight - COLLAPSED_WIDTH - INSET);
|
|
253
|
+
}
|
|
254
|
+
window.addEventListener('resize', () => {
|
|
255
|
+
const rect = host.getBoundingClientRect();
|
|
256
|
+
place(rect.left, rect.top);
|
|
257
|
+
});
|
|
258
|
+
// 8. Let the logo button drag the overlay without triggering its click action.
|
|
259
|
+
let pointerId = null;
|
|
260
|
+
let dragging = false;
|
|
261
|
+
let startX = 0;
|
|
262
|
+
let startY = 0;
|
|
263
|
+
let originLeft = 0;
|
|
264
|
+
let originTop = 0;
|
|
265
|
+
const suppressNextClick = (e) => {
|
|
266
|
+
e.preventDefault();
|
|
267
|
+
e.stopPropagation();
|
|
268
|
+
logoBtn.removeEventListener('click', suppressNextClick, true);
|
|
269
|
+
};
|
|
270
|
+
logoBtn.addEventListener('click', handleLogoClick);
|
|
271
|
+
logoBtn.addEventListener('pointerdown', (e) => {
|
|
272
|
+
if (e.button !== 0 && e.pointerType === 'mouse')
|
|
273
|
+
return;
|
|
274
|
+
pointerId = e.pointerId;
|
|
275
|
+
dragging = false;
|
|
276
|
+
startX = e.clientX;
|
|
277
|
+
startY = e.clientY;
|
|
278
|
+
const rect = host.getBoundingClientRect();
|
|
279
|
+
originLeft = rect.left;
|
|
280
|
+
originTop = rect.top;
|
|
281
|
+
logoBtn.setPointerCapture(pointerId);
|
|
282
|
+
});
|
|
283
|
+
logoBtn.addEventListener('pointermove', (e) => {
|
|
284
|
+
if (e.pointerId !== pointerId)
|
|
285
|
+
return;
|
|
286
|
+
const dx = e.clientX - startX;
|
|
287
|
+
const dy = e.clientY - startY;
|
|
288
|
+
if (!dragging) {
|
|
289
|
+
if (Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD)
|
|
290
|
+
return;
|
|
291
|
+
dragging = true;
|
|
292
|
+
logoBtn.classList.add('kapi-dragging');
|
|
293
|
+
logoBtn.addEventListener('click', suppressNextClick, true);
|
|
294
|
+
stopInspecting();
|
|
295
|
+
}
|
|
296
|
+
place(originLeft + dx, originTop + dy);
|
|
297
|
+
});
|
|
298
|
+
const endDrag = (e) => {
|
|
299
|
+
if (e.pointerId !== pointerId)
|
|
300
|
+
return;
|
|
301
|
+
if (logoBtn.hasPointerCapture(pointerId))
|
|
302
|
+
logoBtn.releasePointerCapture(pointerId);
|
|
303
|
+
pointerId = null;
|
|
304
|
+
if (dragging) {
|
|
305
|
+
logoBtn.classList.remove('kapi-dragging');
|
|
306
|
+
const rect = host.getBoundingClientRect();
|
|
307
|
+
savePosition({ left: rect.left, top: rect.top });
|
|
308
|
+
if (expanded)
|
|
309
|
+
startInspecting();
|
|
310
|
+
}
|
|
311
|
+
dragging = false;
|
|
312
|
+
};
|
|
313
|
+
logoBtn.addEventListener('pointerup', endDrag);
|
|
314
|
+
logoBtn.addEventListener('pointercancel', endDrag);
|
|
315
|
+
}
|
|
316
|
+
// Insert as soon as the page has a body to receive the overlay.
|
|
317
|
+
if (document.readyState === 'loading') {
|
|
318
|
+
document.addEventListener('DOMContentLoaded', () => insertOverlay(), { once: true });
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
insertOverlay();
|
|
322
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Talks to the dev server over Vite's built-in HMR websocket (custom events)
|
|
2
|
+
// rather than a dedicated kapi server on its own port. `import.meta.hot` is
|
|
3
|
+
// injected by Vite into the modules it serves in dev, so the overlay already
|
|
4
|
+
// has a live channel to the dev server — no port discovery, no second socket.
|
|
5
|
+
let onCommentsDone = null;
|
|
6
|
+
let onCommentsProcessing = null;
|
|
7
|
+
let onCommentsError = null;
|
|
8
|
+
export function connectSocket() {
|
|
9
|
+
const hot = import.meta.hot;
|
|
10
|
+
if (!hot) {
|
|
11
|
+
console.error('[kapi] no HMR channel — is the dev server running with the kapi plugin?');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
hot.on('kapi:done', () => onCommentsDone?.());
|
|
15
|
+
hot.on('kapi:processing', (data) => onCommentsProcessing?.(data.status));
|
|
16
|
+
hot.on('kapi:error', (data) => onCommentsError?.(data.message));
|
|
17
|
+
}
|
|
18
|
+
export function sendComments(prompt) {
|
|
19
|
+
const hot = import.meta.hot;
|
|
20
|
+
if (!hot)
|
|
21
|
+
return console.error('[kapi] cannot send comments: no HMR channel');
|
|
22
|
+
hot.send('kapi:submit', { prompt });
|
|
23
|
+
}
|
|
24
|
+
export function stopComments() {
|
|
25
|
+
const hot = import.meta.hot;
|
|
26
|
+
if (!hot)
|
|
27
|
+
return console.error('[kapi] cannot stop comments: no HMR channel');
|
|
28
|
+
hot.send('kapi:stop', {});
|
|
29
|
+
}
|
|
30
|
+
export function setOnCommentsDone(callback) {
|
|
31
|
+
onCommentsDone = callback;
|
|
32
|
+
}
|
|
33
|
+
export function setOnCommentsProcessing(callback) {
|
|
34
|
+
onCommentsProcessing = callback;
|
|
35
|
+
}
|
|
36
|
+
export function setOnCommentsError(callback) {
|
|
37
|
+
onCommentsError = callback;
|
|
38
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
all: initial;
|
|
3
|
+
color-scheme: dark;
|
|
4
|
+
|
|
5
|
+
/* green-500, matches the hover highlight */
|
|
6
|
+
--kapi-marker-color: 34, 197, 94;
|
|
7
|
+
--kapi-marker-size: 22px;
|
|
8
|
+
/* shared fixed width for both the composer and the submitted tooltip */
|
|
9
|
+
--kapi-panel-width: 240px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.kapi-comment {
|
|
13
|
+
position: fixed;
|
|
14
|
+
top: 0;
|
|
15
|
+
left: 0;
|
|
16
|
+
z-index: 2147483647;
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 8px;
|
|
20
|
+
pointer-events: none;
|
|
21
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
22
|
+
/* position() sets --kapi-x/--kapi-y; kept as vars so the exit animation can
|
|
23
|
+
compose scale on top of the translate without clobbering positioning. */
|
|
24
|
+
transform: translate(var(--kapi-x, 0px), var(--kapi-y, 0px));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.kapi-comment.kapi-leaving {
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
animation: kapi-comment-out 200ms ease forwards;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@keyframes kapi-comment-out {
|
|
33
|
+
to {
|
|
34
|
+
opacity: 0;
|
|
35
|
+
filter: blur(3px);
|
|
36
|
+
transform: translate(var(--kapi-x, 0px), var(--kapi-y, 0px)) scale(0.9);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.kapi-comment-marker {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
box-sizing: border-box;
|
|
45
|
+
width: var(--kapi-marker-size);
|
|
46
|
+
height: var(--kapi-marker-size);
|
|
47
|
+
border-radius: 50%;
|
|
48
|
+
background: rgb(var(--kapi-marker-color));
|
|
49
|
+
color: #fff;
|
|
50
|
+
font-size: 11px;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
|
|
53
|
+
pointer-events: auto;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
flex: none;
|
|
56
|
+
user-select: none;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.kapi-comment-marker-enter {
|
|
60
|
+
animation: kapi-marker-in 220ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@keyframes kapi-marker-in {
|
|
64
|
+
from {
|
|
65
|
+
transform: scale(0.5);
|
|
66
|
+
}
|
|
67
|
+
to {
|
|
68
|
+
transform: scale(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.kapi-comment-tooltip,
|
|
73
|
+
.kapi-comment-composer {
|
|
74
|
+
display: none;
|
|
75
|
+
position: absolute;
|
|
76
|
+
left: calc(var(--kapi-marker-size) + 8px);
|
|
77
|
+
top: 50%;
|
|
78
|
+
transform: translateY(-50%);
|
|
79
|
+
width: var(--kapi-panel-width);
|
|
80
|
+
box-sizing: border-box;
|
|
81
|
+
border-radius: 12px;
|
|
82
|
+
background: #1e1e1f;
|
|
83
|
+
box-shadow:
|
|
84
|
+
inset 0 0 0 1px rgba(255, 255, 255, 0.14),
|
|
85
|
+
0 2px 4px rgba(0, 0, 0, 0.2),
|
|
86
|
+
0 8px 16px rgba(0, 0, 0, 0.2);
|
|
87
|
+
pointer-events: auto;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.kapi-comment-tooltip {
|
|
91
|
+
flex-direction: column;
|
|
92
|
+
gap: 2px;
|
|
93
|
+
padding: 8px 14px;
|
|
94
|
+
color: #fff;
|
|
95
|
+
font-size: 13px;
|
|
96
|
+
line-height: 1.3;
|
|
97
|
+
white-space: normal;
|
|
98
|
+
overflow-wrap: break-word;
|
|
99
|
+
animation: kapi-tooltip-in 220ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@keyframes kapi-tooltip-in {
|
|
103
|
+
from {
|
|
104
|
+
opacity: 0;
|
|
105
|
+
transform: translateY(-50%) translateY(8px);
|
|
106
|
+
}
|
|
107
|
+
to {
|
|
108
|
+
opacity: 1;
|
|
109
|
+
transform: translateY(-50%);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.kapi-comment.kapi-hovering .kapi-comment-tooltip {
|
|
114
|
+
display: flex;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.kapi-comment-tooltip-component {
|
|
118
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
119
|
+
font-size: 11px;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
color: rgb(var(--kapi-marker-color));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.kapi-comment-tooltip-source {
|
|
125
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
126
|
+
font-size: 11px;
|
|
127
|
+
color: rgba(255, 255, 255, 0.5);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.kapi-comment-composer {
|
|
131
|
+
display: flex;
|
|
132
|
+
flex-direction: column;
|
|
133
|
+
top: 0;
|
|
134
|
+
transform: none;
|
|
135
|
+
animation: kapi-composer-in 220ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.kapi-comment-composer-input-row {
|
|
139
|
+
padding: 8px 0 4px 14px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.kapi-comment-composer-actions {
|
|
143
|
+
display: flex;
|
|
144
|
+
justify-content: flex-end;
|
|
145
|
+
align-items: center;
|
|
146
|
+
padding: 6px;
|
|
147
|
+
gap: 6px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.kapi-comment-composer-actions:has(.kapi-comment-delete) {
|
|
151
|
+
justify-content: space-between;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.kapi-comment-delete {
|
|
155
|
+
all: unset;
|
|
156
|
+
display: flex;
|
|
157
|
+
align-items: center;
|
|
158
|
+
justify-content: center;
|
|
159
|
+
box-sizing: border-box;
|
|
160
|
+
width: 24px;
|
|
161
|
+
height: 24px;
|
|
162
|
+
border-radius: 8px;
|
|
163
|
+
background: transparent;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
flex: none;
|
|
166
|
+
transition: background 150ms ease;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.kapi-comment-delete:hover {
|
|
170
|
+
background: rgba(239, 68, 68, 0.2);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.kapi-delete-icon {
|
|
174
|
+
height: 13px;
|
|
175
|
+
width: auto;
|
|
176
|
+
opacity: 0.5;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.kapi-comment-delete:hover .kapi-delete-icon,
|
|
180
|
+
.kapi-comment-delete:active .kapi-delete-icon {
|
|
181
|
+
opacity: 1;
|
|
182
|
+
filter: brightness(0) saturate(100%) invert(31%) sepia(78%) saturate(2043%) hue-rotate(354deg) brightness(105%) contrast(104%);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.kapi-comment-delete:active {
|
|
186
|
+
background: rgba(239, 68, 68, 0.3);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@keyframes kapi-composer-in {
|
|
190
|
+
from {
|
|
191
|
+
opacity: 0;
|
|
192
|
+
transform: translateY(8px);
|
|
193
|
+
}
|
|
194
|
+
to {
|
|
195
|
+
opacity: 1;
|
|
196
|
+
transform: translateY(0);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.kapi-comment-input {
|
|
201
|
+
all: unset;
|
|
202
|
+
box-sizing: border-box;
|
|
203
|
+
display: block;
|
|
204
|
+
width: 100%;
|
|
205
|
+
max-height: 120px;
|
|
206
|
+
overflow-y: auto;
|
|
207
|
+
resize: none;
|
|
208
|
+
color: #fff;
|
|
209
|
+
font-family: inherit;
|
|
210
|
+
font-size: 13px;
|
|
211
|
+
line-height: 1.3;
|
|
212
|
+
padding: 0;
|
|
213
|
+
scrollbar-width: thin;
|
|
214
|
+
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.kapi-comment-input::-webkit-scrollbar {
|
|
218
|
+
width: 4px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.kapi-comment-input::-webkit-scrollbar-track {
|
|
222
|
+
background: transparent;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.kapi-comment-input::-webkit-scrollbar-thumb {
|
|
226
|
+
background: rgba(255, 255, 255, 0.3);
|
|
227
|
+
border-radius: 999px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.kapi-comment-input::placeholder {
|
|
231
|
+
color: rgba(255, 255, 255, 0.4);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.kapi-comment-send {
|
|
235
|
+
all: unset;
|
|
236
|
+
display: flex;
|
|
237
|
+
align-items: center;
|
|
238
|
+
justify-content: center;
|
|
239
|
+
box-sizing: border-box;
|
|
240
|
+
width: 24px;
|
|
241
|
+
height: 24px;
|
|
242
|
+
border-radius: 8px;
|
|
243
|
+
background: #fff;
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
flex: none;
|
|
246
|
+
}
|