@ui-context-kit/overlay 0.1.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.
@@ -0,0 +1,823 @@
1
+ // src/ui/overlay.ts
2
+ var hostEl = null;
3
+ var shadowRoot = null;
4
+ var highlightEl = null;
5
+ function createOverlay() {
6
+ hostEl = document.createElement("div");
7
+ hostEl.id = "ui-context-host";
8
+ hostEl.style.cssText = "position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647;pointer-events:none;";
9
+ document.body.appendChild(hostEl);
10
+ shadowRoot = hostEl.attachShadow({ mode: "open" });
11
+ const style = document.createElement("style");
12
+ style.textContent = `
13
+ .highlight {
14
+ position: fixed;
15
+ border: 2px solid #3b82f6;
16
+ background: rgba(59, 130, 246, 0.1);
17
+ pointer-events: none;
18
+ transition: all 0.1s ease;
19
+ border-radius: 4px;
20
+ z-index: 2147483647;
21
+ }
22
+ .highlight-label {
23
+ position: absolute;
24
+ top: -24px;
25
+ left: -2px;
26
+ background: #3b82f6;
27
+ color: white;
28
+ font-size: 11px;
29
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
30
+ padding: 2px 6px;
31
+ border-radius: 3px 3px 0 0;
32
+ white-space: nowrap;
33
+ pointer-events: none;
34
+ }
35
+ `;
36
+ shadowRoot.appendChild(style);
37
+ highlightEl = document.createElement("div");
38
+ highlightEl.className = "highlight";
39
+ highlightEl.style.display = "none";
40
+ const labelEl = document.createElement("div");
41
+ labelEl.className = "highlight-label";
42
+ highlightEl.appendChild(labelEl);
43
+ shadowRoot.appendChild(highlightEl);
44
+ return {
45
+ highlight(rect, label) {
46
+ if (!rect || !highlightEl) {
47
+ if (highlightEl) highlightEl.style.display = "none";
48
+ return;
49
+ }
50
+ highlightEl.style.display = "block";
51
+ highlightEl.style.top = rect.top + "px";
52
+ highlightEl.style.left = rect.left + "px";
53
+ highlightEl.style.width = rect.width + "px";
54
+ highlightEl.style.height = rect.height + "px";
55
+ const lbl = highlightEl.querySelector(".highlight-label");
56
+ if (lbl && label) {
57
+ lbl.textContent = label;
58
+ lbl.style.display = "block";
59
+ } else if (lbl) {
60
+ lbl.style.display = "none";
61
+ }
62
+ },
63
+ destroy() {
64
+ hostEl?.remove();
65
+ hostEl = null;
66
+ shadowRoot = null;
67
+ highlightEl = null;
68
+ }
69
+ };
70
+ }
71
+
72
+ // src/ui/toast.ts
73
+ var toastContainer = null;
74
+ var toastShadow = null;
75
+ function ensureContainer() {
76
+ if (toastContainer) return;
77
+ toastContainer = document.createElement("div");
78
+ toastContainer.id = "ui-context-toast";
79
+ toastContainer.style.cssText = "position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647;pointer-events:none;";
80
+ document.body.appendChild(toastContainer);
81
+ toastShadow = toastContainer.attachShadow({ mode: "open" });
82
+ const style = document.createElement("style");
83
+ style.textContent = `
84
+ .toast {
85
+ position: fixed;
86
+ bottom: 24px;
87
+ right: 24px;
88
+ background: #1e293b;
89
+ color: #f1f5f9;
90
+ padding: 12px 20px;
91
+ border-radius: 8px;
92
+ font-size: 13px;
93
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
94
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
95
+ pointer-events: auto;
96
+ animation: slideIn 0.3s ease, fadeOut 0.3s ease 2.7s;
97
+ z-index: 2147483647;
98
+ }
99
+ @keyframes slideIn { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
100
+ @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
101
+ `;
102
+ toastShadow.appendChild(style);
103
+ }
104
+ function showToast(message, duration = 3e3) {
105
+ ensureContainer();
106
+ if (!toastShadow) return;
107
+ const el = document.createElement("div");
108
+ el.className = "toast";
109
+ el.textContent = message;
110
+ toastShadow.appendChild(el);
111
+ setTimeout(() => el.remove(), duration);
112
+ }
113
+
114
+ // src/ui/toolbar.ts
115
+ function createToolbar(opts) {
116
+ const host = document.createElement("div");
117
+ host.style.cssText = "position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647;";
118
+ document.body.appendChild(host);
119
+ const shadow = host.attachShadow({ mode: "open" });
120
+ const style = document.createElement("style");
121
+ style.textContent = `
122
+ .toolbar {
123
+ position: fixed;
124
+ bottom: 24px;
125
+ left: 50%;
126
+ transform: translateX(-50%);
127
+ background: #1e293b;
128
+ border: 1px solid #334155;
129
+ border-radius: 12px;
130
+ padding: 8px 12px;
131
+ display: flex;
132
+ gap: 6px;
133
+ align-items: center;
134
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
135
+ box-shadow: 0 4px 20px rgba(0,0,0,0.4);
136
+ }
137
+ .toolbar-btn {
138
+ background: #3b82f6;
139
+ color: white;
140
+ border: none;
141
+ padding: 6px 14px;
142
+ border-radius: 6px;
143
+ font-size: 12px;
144
+ font-weight: 500;
145
+ cursor: pointer;
146
+ white-space: nowrap;
147
+ }
148
+ .toolbar-btn:hover { background: #2563eb; }
149
+ .toolbar-btn.secondary { background: #475569; }
150
+ .toolbar-btn.secondary:hover { background: #64748b; }
151
+ .toolbar-btn.freeze { background: #475569; }
152
+ .toolbar-btn.freeze:hover { background: #64748b; }
153
+ .toolbar-btn.freeze.active { background: #f59e0b; color: #1e293b; }
154
+ .toolbar-btn.freeze.active:hover { background: #d97706; }
155
+ .toolbar-label {
156
+ color: #94a3b8;
157
+ font-size: 12px;
158
+ padding: 0 4px;
159
+ }
160
+ .badge {
161
+ display: inline-block;
162
+ background: #f59e0b;
163
+ color: #1e293b;
164
+ font-size: 10px;
165
+ font-weight: 700;
166
+ padding: 1px 5px;
167
+ border-radius: 10px;
168
+ margin-left: 4px;
169
+ }
170
+ .badge.hidden { display: none; }
171
+ .divider {
172
+ width: 1px;
173
+ height: 20px;
174
+ background: #334155;
175
+ }
176
+ `;
177
+ shadow.appendChild(style);
178
+ const toolbar = document.createElement("div");
179
+ toolbar.className = "toolbar";
180
+ toolbar.innerHTML = `
181
+ <span class="toolbar-label">UI Context</span>
182
+ <div class="divider"></div>
183
+ <button class="toolbar-btn capture">Capture</button>
184
+ <button class="toolbar-btn secondary capture-all" style="display:none">Capture All <span class="badge hidden">0</span></button>
185
+ <div class="divider"></div>
186
+ <button class="toolbar-btn freeze" title="Freeze DOM (Ctrl+Shift+F)">Freeze</button>
187
+ <button class="toolbar-btn secondary close">\u2715</button>
188
+ `;
189
+ shadow.appendChild(toolbar);
190
+ const captureBtn = toolbar.querySelector(".capture");
191
+ const captureAllBtn = toolbar.querySelector(".capture-all");
192
+ const freezeBtn = toolbar.querySelector(".freeze");
193
+ const closeBtn = toolbar.querySelector(".close");
194
+ const badge = toolbar.querySelector(".badge");
195
+ captureBtn.addEventListener("click", opts.onCapture);
196
+ captureAllBtn.addEventListener("click", () => opts.onCaptureAll?.());
197
+ freezeBtn.addEventListener("click", () => opts.onFreeze?.());
198
+ closeBtn.addEventListener("click", opts.onClose);
199
+ return {
200
+ el: host,
201
+ destroy() {
202
+ host.remove();
203
+ },
204
+ updateBadge(count) {
205
+ if (count > 0) {
206
+ captureAllBtn.style.display = "";
207
+ badge.textContent = String(count);
208
+ badge.classList.remove("hidden");
209
+ } else {
210
+ captureAllBtn.style.display = "none";
211
+ badge.classList.add("hidden");
212
+ }
213
+ },
214
+ updateFreezeState(frozen) {
215
+ if (frozen) {
216
+ freezeBtn.classList.add("active");
217
+ freezeBtn.textContent = "Frozen";
218
+ } else {
219
+ freezeBtn.classList.remove("active");
220
+ freezeBtn.textContent = "Freeze";
221
+ }
222
+ }
223
+ };
224
+ }
225
+
226
+ // src/fiber-walker.ts
227
+ function extractComponentHierarchy(el) {
228
+ const fiber = getFiber(el);
229
+ if (!fiber) return [];
230
+ const hierarchy = [];
231
+ let current = fiber;
232
+ while (current) {
233
+ if (typeof current.type === "function" || typeof current.type === "object") {
234
+ const name = typeof current.type === "function" ? current.type.displayName || current.type.name : current.type?.displayName || current.type?.render?.displayName || current.type?.render?.name;
235
+ if (name && !name.startsWith("_") && name !== "Fragment") {
236
+ hierarchy.unshift(name);
237
+ }
238
+ }
239
+ current = current.return;
240
+ }
241
+ return hierarchy;
242
+ }
243
+ function getFiber(el) {
244
+ const key = Object.keys(el).find(
245
+ (k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$")
246
+ );
247
+ return key ? el[key] : null;
248
+ }
249
+
250
+ // src/context-extractor.ts
251
+ var STYLE_PROPERTIES = [
252
+ "padding",
253
+ "paddingTop",
254
+ "paddingRight",
255
+ "paddingBottom",
256
+ "paddingLeft",
257
+ "margin",
258
+ "marginTop",
259
+ "marginRight",
260
+ "marginBottom",
261
+ "marginLeft",
262
+ "backgroundColor",
263
+ "color",
264
+ "fontSize",
265
+ "fontWeight",
266
+ "fontFamily",
267
+ "borderRadius",
268
+ "display",
269
+ "flexDirection",
270
+ "justifyContent",
271
+ "alignItems",
272
+ "gap",
273
+ "width",
274
+ "height",
275
+ "minWidth",
276
+ "minHeight",
277
+ "maxWidth",
278
+ "maxHeight",
279
+ "overflow",
280
+ "position",
281
+ "zIndex",
282
+ "opacity",
283
+ "border",
284
+ "boxShadow",
285
+ "lineHeight",
286
+ "letterSpacing",
287
+ "textAlign"
288
+ ];
289
+ function extractSource(el) {
290
+ const file = el.getAttribute("data-source-file");
291
+ const line = el.getAttribute("data-source-line");
292
+ const col = el.getAttribute("data-source-col");
293
+ if (!file || !line) return null;
294
+ return {
295
+ file,
296
+ line: parseInt(line, 10),
297
+ col: parseInt(col || "0", 10),
298
+ component: el.getAttribute("data-source-component") || void 0
299
+ };
300
+ }
301
+ function extractContext(el) {
302
+ const computed = getComputedStyle(el);
303
+ const rect = el.getBoundingClientRect();
304
+ const styles = {};
305
+ for (const prop of STYLE_PROPERTIES) {
306
+ const value = computed.getPropertyValue(
307
+ prop.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase())
308
+ );
309
+ if (value && value !== "none" && value !== "normal" && value !== "auto" && value !== "0px" && value !== "rgba(0, 0, 0, 0)") {
310
+ styles[prop] = value;
311
+ }
312
+ }
313
+ return {
314
+ source: extractSource(el),
315
+ tagName: el.tagName.toLowerCase(),
316
+ classes: typeof el.className === "string" ? el.className : "",
317
+ computedStyles: styles,
318
+ dimensions: { width: rect.width, height: rect.height, x: rect.x, y: rect.y },
319
+ componentHierarchy: extractComponentHierarchy(el),
320
+ url: window.location.href
321
+ };
322
+ }
323
+
324
+ // src/screenshot.ts
325
+ async function captureElementScreenshot(el) {
326
+ try {
327
+ const rect = el.getBoundingClientRect();
328
+ if (rect.width === 0 || rect.height === 0) return void 0;
329
+ const canvas = document.createElement("canvas");
330
+ const dpr = window.devicePixelRatio || 1;
331
+ const padding = 8;
332
+ const captureWidth = Math.ceil(rect.width + padding * 2);
333
+ const captureHeight = Math.ceil(rect.height + padding * 2);
334
+ canvas.width = captureWidth * dpr;
335
+ canvas.height = captureHeight * dpr;
336
+ canvas.style.width = captureWidth + "px";
337
+ canvas.style.height = captureHeight + "px";
338
+ const ctx = canvas.getContext("2d");
339
+ if (!ctx) return void 0;
340
+ ctx.scale(dpr, dpr);
341
+ const screenshot = await captureViaSvgForeignObject(el, rect, ctx, canvas, padding);
342
+ if (screenshot) return screenshot;
343
+ return await captureViaRange(el, rect, canvas, ctx, padding);
344
+ } catch {
345
+ return void 0;
346
+ }
347
+ }
348
+ async function captureViaSvgForeignObject(el, rect, ctx, canvas, padding) {
349
+ try {
350
+ const clone = el.cloneNode(true);
351
+ copyComputedStyles(el, clone);
352
+ const width = Math.ceil(rect.width + padding * 2);
353
+ const height = Math.ceil(rect.height + padding * 2);
354
+ const svgNs = "http://www.w3.org/2000/svg";
355
+ const svg = document.createElementNS(svgNs, "svg");
356
+ svg.setAttribute("width", String(width));
357
+ svg.setAttribute("height", String(height));
358
+ svg.setAttribute("xmlns", svgNs);
359
+ const foreignObject = document.createElementNS(svgNs, "foreignObject");
360
+ foreignObject.setAttribute("width", "100%");
361
+ foreignObject.setAttribute("height", "100%");
362
+ const container = document.createElement("div");
363
+ container.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
364
+ container.style.cssText = `
365
+ width: ${width}px;
366
+ height: ${height}px;
367
+ display: flex;
368
+ align-items: center;
369
+ justify-content: center;
370
+ background: white;
371
+ padding: ${padding}px;
372
+ box-sizing: border-box;
373
+ `;
374
+ container.appendChild(clone);
375
+ foreignObject.appendChild(container);
376
+ svg.appendChild(foreignObject);
377
+ const svgString = new XMLSerializer().serializeToString(svg);
378
+ const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
379
+ const url = URL.createObjectURL(svgBlob);
380
+ return new Promise((resolve) => {
381
+ const img = new Image();
382
+ img.onload = () => {
383
+ ctx.drawImage(img, 0, 0);
384
+ URL.revokeObjectURL(url);
385
+ try {
386
+ resolve(canvas.toDataURL("image/png"));
387
+ } catch {
388
+ resolve(void 0);
389
+ }
390
+ };
391
+ img.onerror = () => {
392
+ URL.revokeObjectURL(url);
393
+ resolve(void 0);
394
+ };
395
+ img.src = url;
396
+ });
397
+ } catch {
398
+ return void 0;
399
+ }
400
+ }
401
+ async function captureViaRange(_el, rect, canvas, ctx, padding) {
402
+ try {
403
+ const width = canvas.width / (window.devicePixelRatio || 1);
404
+ const height = canvas.height / (window.devicePixelRatio || 1);
405
+ ctx.fillStyle = "#f8fafc";
406
+ ctx.fillRect(0, 0, width, height);
407
+ ctx.strokeStyle = "#3b82f6";
408
+ ctx.lineWidth = 2;
409
+ ctx.strokeRect(padding, padding, rect.width, rect.height);
410
+ ctx.fillStyle = "#64748b";
411
+ ctx.font = "11px -apple-system, BlinkMacSystemFont, sans-serif";
412
+ ctx.fillText(
413
+ `${Math.round(rect.width)} x ${Math.round(rect.height)}px`,
414
+ padding + 4,
415
+ padding + rect.height / 2 + 4
416
+ );
417
+ return canvas.toDataURL("image/png");
418
+ } catch {
419
+ return void 0;
420
+ }
421
+ }
422
+ function copyComputedStyles(source, target) {
423
+ const computed = getComputedStyle(source);
424
+ const important = [
425
+ "display",
426
+ "width",
427
+ "height",
428
+ "padding",
429
+ "margin",
430
+ "border",
431
+ "border-radius",
432
+ "background",
433
+ "background-color",
434
+ "color",
435
+ "font-size",
436
+ "font-weight",
437
+ "font-family",
438
+ "line-height",
439
+ "text-align",
440
+ "box-shadow",
441
+ "opacity",
442
+ "flex-direction",
443
+ "justify-content",
444
+ "align-items",
445
+ "gap",
446
+ "overflow"
447
+ ];
448
+ for (const prop of important) {
449
+ target.style.setProperty(prop, computed.getPropertyValue(prop));
450
+ }
451
+ const sourceChildren = source.children;
452
+ const targetChildren = target.children;
453
+ for (let i = 0; i < sourceChildren.length && i < targetChildren.length; i++) {
454
+ if (sourceChildren[i] instanceof HTMLElement && targetChildren[i] instanceof HTMLElement) {
455
+ copyComputedStyles(sourceChildren[i], targetChildren[i]);
456
+ }
457
+ }
458
+ }
459
+
460
+ // src/selection.ts
461
+ function createSelectionHandler(opts) {
462
+ let isActive = false;
463
+ let selectedEl = null;
464
+ const multiSelected = /* @__PURE__ */ new Set();
465
+ function handleMouseMove(e) {
466
+ if (!isActive) return;
467
+ const target = e.target;
468
+ if (target.id === "ui-context-host" || target.id === "ui-context-toast" || target.closest("#ui-context-host")) return;
469
+ opts.onHover(target);
470
+ }
471
+ function handleClick(e) {
472
+ if (!isActive) return;
473
+ e.preventDefault();
474
+ e.stopPropagation();
475
+ const target = e.target;
476
+ if (target.id === "ui-context-host" || target.closest("#ui-context-host")) return;
477
+ if (e.shiftKey) {
478
+ if (multiSelected.has(target)) {
479
+ multiSelected.delete(target);
480
+ target.style.outline = "";
481
+ } else {
482
+ multiSelected.add(target);
483
+ target.style.outline = "2px dashed #f59e0b";
484
+ }
485
+ opts.onSelectionChange?.(multiSelected.size);
486
+ return;
487
+ }
488
+ selectedEl = target;
489
+ const context = extractContext(target);
490
+ captureElementScreenshot(target).then((screenshot) => {
491
+ if (screenshot) context.screenshotDataUrl = screenshot;
492
+ opts.onSelect(context);
493
+ });
494
+ }
495
+ return {
496
+ activate() {
497
+ isActive = true;
498
+ document.addEventListener("mousemove", handleMouseMove, true);
499
+ document.addEventListener("click", handleClick, true);
500
+ document.body.style.cursor = "crosshair";
501
+ },
502
+ deactivate() {
503
+ isActive = false;
504
+ selectedEl = null;
505
+ for (const el of multiSelected) {
506
+ el.style.outline = "";
507
+ }
508
+ multiSelected.clear();
509
+ document.removeEventListener("mousemove", handleMouseMove, true);
510
+ document.removeEventListener("click", handleClick, true);
511
+ document.body.style.cursor = "";
512
+ opts.onHover(null);
513
+ },
514
+ get isActive() {
515
+ return isActive;
516
+ },
517
+ get selectedElement() {
518
+ return selectedEl;
519
+ },
520
+ get multiSelectedElements() {
521
+ return multiSelected;
522
+ },
523
+ get multiSelectedCount() {
524
+ return multiSelected.size;
525
+ },
526
+ async captureMultiSelection() {
527
+ const contexts = [];
528
+ for (const el of multiSelected) {
529
+ const ctx = extractContext(el);
530
+ const screenshot = await captureElementScreenshot(el);
531
+ if (screenshot) ctx.screenshotDataUrl = screenshot;
532
+ contexts.push(ctx);
533
+ }
534
+ return contexts;
535
+ },
536
+ clearMultiSelection() {
537
+ for (const el of multiSelected) {
538
+ el.style.outline = "";
539
+ }
540
+ multiSelected.clear();
541
+ opts.onSelectionChange?.(0);
542
+ }
543
+ };
544
+ }
545
+
546
+ // src/transport.ts
547
+ var ws = null;
548
+ var wsUrl = "";
549
+ function initTransport(url) {
550
+ wsUrl = url || `ws://${location.hostname}:${location.port}`;
551
+ }
552
+ function sendContext(context) {
553
+ return new Promise((resolve) => {
554
+ try {
555
+ if (typeof import.meta !== "undefined" && import.meta.hot) {
556
+ import.meta.hot.send("ui-context:capture", context);
557
+ resolve(true);
558
+ return;
559
+ }
560
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
561
+ ws = new WebSocket(wsUrl);
562
+ ws.onopen = () => {
563
+ ws.send(JSON.stringify({ type: "ui-context:capture", data: context }));
564
+ resolve(true);
565
+ };
566
+ ws.onerror = () => resolve(false);
567
+ } else {
568
+ ws.send(JSON.stringify({ type: "ui-context:capture", data: context }));
569
+ resolve(true);
570
+ }
571
+ } catch {
572
+ resolve(false);
573
+ }
574
+ });
575
+ }
576
+
577
+ // src/ui/onboarding.ts
578
+ var STORAGE_KEY = "ui-context-kit-onboarding-dismissed";
579
+ function showOnboarding(shortcut) {
580
+ try {
581
+ if (localStorage.getItem(STORAGE_KEY)) return;
582
+ } catch {
583
+ return;
584
+ }
585
+ const host = document.createElement("div");
586
+ host.style.cssText = "position:fixed;top:0;left:0;width:0;height:0;z-index:2147483646;";
587
+ document.body.appendChild(host);
588
+ const shadow = host.attachShadow({ mode: "open" });
589
+ const style = document.createElement("style");
590
+ style.textContent = `
591
+ .banner {
592
+ position: fixed;
593
+ bottom: 20px;
594
+ left: 50%;
595
+ transform: translateX(-50%);
596
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
597
+ border: 1px solid #334155;
598
+ border-radius: 12px;
599
+ padding: 12px 20px;
600
+ display: flex;
601
+ gap: 12px;
602
+ align-items: center;
603
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
604
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
605
+ animation: slideUp 0.4s ease;
606
+ max-width: 480px;
607
+ }
608
+ @keyframes slideUp {
609
+ from { opacity: 0; transform: translateX(-50%) translateY(20px); }
610
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
611
+ }
612
+ .icon {
613
+ font-size: 20px;
614
+ flex-shrink: 0;
615
+ }
616
+ .text {
617
+ color: #e2e8f0;
618
+ font-size: 13px;
619
+ line-height: 1.4;
620
+ }
621
+ .text strong {
622
+ color: #60a5fa;
623
+ }
624
+ .kbd {
625
+ display: inline-block;
626
+ background: #334155;
627
+ color: #e2e8f0;
628
+ padding: 1px 6px;
629
+ border-radius: 4px;
630
+ font-size: 12px;
631
+ font-family: monospace;
632
+ border: 1px solid #475569;
633
+ }
634
+ .close-btn {
635
+ background: none;
636
+ border: none;
637
+ color: #64748b;
638
+ cursor: pointer;
639
+ padding: 4px;
640
+ font-size: 16px;
641
+ flex-shrink: 0;
642
+ }
643
+ .close-btn:hover { color: #94a3b8; }
644
+ `;
645
+ shadow.appendChild(style);
646
+ const shortcutDisplay = shortcut.split("+").map(
647
+ (k) => k.charAt(0).toUpperCase() + k.slice(1)
648
+ ).join(" + ");
649
+ const banner = document.createElement("div");
650
+ banner.className = "banner";
651
+ banner.innerHTML = `
652
+ <span class="icon">\u{1F3AF}</span>
653
+ <span class="text">
654
+ <strong>UI Context Kit</strong> is ready!
655
+ Press <kbd class="kbd">${shortcutDisplay}</kbd> to select any UI element and capture its context for AI.
656
+ Use <strong>Shift+Click</strong> for multi-select, <strong>Ctrl+Shift+F</strong> to freeze state.
657
+ </span>
658
+ <button class="close-btn" title="Dismiss">\u2715</button>
659
+ `;
660
+ shadow.appendChild(banner);
661
+ banner.querySelector(".close-btn").addEventListener("click", () => {
662
+ host.remove();
663
+ try {
664
+ localStorage.setItem(STORAGE_KEY, "1");
665
+ } catch {
666
+ }
667
+ });
668
+ setTimeout(() => {
669
+ if (host.parentNode) {
670
+ banner.style.transition = "opacity 0.3s ease";
671
+ banner.style.opacity = "0";
672
+ setTimeout(() => host.remove(), 300);
673
+ }
674
+ }, 8e3);
675
+ }
676
+
677
+ // src/index.ts
678
+ function initUIContext(options = {}) {
679
+ const { shortcut = "alt+x" } = options;
680
+ initTransport(options.wsUrl);
681
+ let overlay = null;
682
+ let toolbar = null;
683
+ let isFrozen = false;
684
+ let frozenObservers = [];
685
+ const selection = createSelectionHandler({
686
+ onHover(el) {
687
+ if (!el) {
688
+ overlay?.highlight(null);
689
+ return;
690
+ }
691
+ const rect = el.getBoundingClientRect();
692
+ const source = extractSource(el);
693
+ const label = source ? `${source.component || el.tagName.toLowerCase()} \xB7 ${source.file}:${source.line}` : el.tagName.toLowerCase();
694
+ overlay?.highlight(rect, label);
695
+ },
696
+ async onSelect(context) {
697
+ showToast("Capturing context...");
698
+ const success = await sendContext(context);
699
+ if (success) {
700
+ showToast("Context captured!");
701
+ } else {
702
+ showToast("Failed to send context. Is the dev server running?");
703
+ }
704
+ },
705
+ onSelectionChange(count) {
706
+ toolbar?.updateBadge(count);
707
+ }
708
+ });
709
+ function activate() {
710
+ if (selection.isActive) return;
711
+ overlay = createOverlay();
712
+ toolbar = createToolbar({
713
+ onCapture() {
714
+ if (selection.selectedElement) {
715
+ const context = extractContext(selection.selectedElement);
716
+ sendContext(context);
717
+ }
718
+ },
719
+ async onCaptureAll() {
720
+ if (selection.multiSelectedCount === 0) {
721
+ showToast("No elements selected. Use Shift+Click to select multiple.");
722
+ return;
723
+ }
724
+ showToast(`Capturing ${selection.multiSelectedCount} elements...`);
725
+ const contexts = await selection.captureMultiSelection();
726
+ const success = await sendContext(contexts);
727
+ if (success) {
728
+ showToast(`${contexts.length} elements captured!`);
729
+ selection.clearMultiSelection();
730
+ } else {
731
+ showToast("Failed to send context. Is the dev server running?");
732
+ }
733
+ },
734
+ onClose() {
735
+ deactivate();
736
+ },
737
+ onFreeze() {
738
+ toggleFreeze();
739
+ }
740
+ });
741
+ selection.activate();
742
+ showToast("UI Context active \u2014 click to capture, Shift+click for multi-select");
743
+ }
744
+ function deactivate() {
745
+ if (isFrozen) unfreezeDOM();
746
+ selection.deactivate();
747
+ overlay?.destroy();
748
+ toolbar?.destroy();
749
+ overlay = null;
750
+ toolbar = null;
751
+ }
752
+ function toggleFreeze() {
753
+ if (isFrozen) {
754
+ unfreezeDOM();
755
+ showToast("DOM unfrozen");
756
+ } else {
757
+ freezeDOM();
758
+ showToast("DOM frozen \u2014 hover states preserved");
759
+ }
760
+ toolbar?.updateFreezeState(isFrozen);
761
+ }
762
+ function freezeDOM() {
763
+ isFrozen = true;
764
+ const observer = new MutationObserver((mutations) => {
765
+ for (const mutation of mutations) {
766
+ for (const node of mutation.addedNodes) {
767
+ if (node instanceof HTMLElement && !node.id?.includes("ui-context")) {
768
+ node.remove();
769
+ }
770
+ }
771
+ if (mutation.type === "attributes" && mutation.target instanceof HTMLElement) {
772
+ if (!mutation.target.id?.includes("ui-context")) {
773
+ const old = mutation.oldValue;
774
+ if (old !== null) {
775
+ mutation.target.setAttribute(mutation.attributeName, old);
776
+ }
777
+ }
778
+ }
779
+ }
780
+ });
781
+ observer.observe(document.body, {
782
+ childList: true,
783
+ subtree: true,
784
+ attributes: true,
785
+ attributeOldValue: true
786
+ });
787
+ frozenObservers.push(observer);
788
+ }
789
+ function unfreezeDOM() {
790
+ isFrozen = false;
791
+ for (const obs of frozenObservers) {
792
+ obs.disconnect();
793
+ }
794
+ frozenObservers = [];
795
+ }
796
+ document.addEventListener("keydown", (e) => {
797
+ const keys = shortcut.split("+");
798
+ const needsAlt = keys.includes("alt");
799
+ const needsCtrl = keys.includes("ctrl");
800
+ const needsShift = keys.includes("shift");
801
+ const mainKey = keys[keys.length - 1].toLowerCase();
802
+ if (e.altKey === needsAlt && e.ctrlKey === needsCtrl && e.shiftKey === needsShift && e.key.toLowerCase() === mainKey) {
803
+ e.preventDefault();
804
+ if (selection.isActive) {
805
+ deactivate();
806
+ } else {
807
+ activate();
808
+ }
809
+ return;
810
+ }
811
+ if (selection.isActive && e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "f") {
812
+ e.preventDefault();
813
+ toggleFreeze();
814
+ }
815
+ });
816
+ showOnboarding(shortcut);
817
+ return { activate, deactivate };
818
+ }
819
+
820
+ export {
821
+ initUIContext
822
+ };
823
+ //# sourceMappingURL=chunk-ISOPDZ5A.js.map