context_drop 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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.md +337 -0
  4. data/Rakefile +20 -0
  5. data/SPEC.md +532 -0
  6. data/app/assets/javascripts/context_drop/context_drop.js +3508 -0
  7. data/app/assets/stylesheets/context_drop/application.css +2 -0
  8. data/app/controllers/context_drop/application_controller.rb +114 -0
  9. data/app/controllers/context_drop/attachments_controller.rb +31 -0
  10. data/app/controllers/context_drop/configs_controller.rb +20 -0
  11. data/app/controllers/context_drop/dashboard/feedbacks_controller.rb +107 -0
  12. data/app/controllers/context_drop/feedbacks_controller.rb +208 -0
  13. data/app/controllers/context_drop/health_controller.rb +9 -0
  14. data/app/controllers/context_drop/mcp_controller.rb +67 -0
  15. data/app/helpers/context_drop/widget_helper.rb +62 -0
  16. data/app/javascript/context_drop/__tests__/widget.test.ts +319 -0
  17. data/app/javascript/context_drop/api-client.ts +87 -0
  18. data/app/javascript/context_drop/element-picker.ts +1004 -0
  19. data/app/javascript/context_drop/icons.ts +35 -0
  20. data/app/javascript/context_drop/index.ts +11 -0
  21. data/app/javascript/context_drop/sanitizer.ts +454 -0
  22. data/app/javascript/context_drop/screenshot.ts +214 -0
  23. data/app/javascript/context_drop/styles.ts +690 -0
  24. data/app/javascript/context_drop/types.ts +110 -0
  25. data/app/javascript/context_drop/widget.ts +1120 -0
  26. data/app/models/context_drop/application_record.rb +7 -0
  27. data/app/models/context_drop/attachment.rb +30 -0
  28. data/app/models/context_drop/element_reference.rb +47 -0
  29. data/app/models/context_drop/feedback.rb +94 -0
  30. data/app/services/context_drop/metadata_sanitizer.rb +215 -0
  31. data/app/services/context_drop/upload_validator.rb +217 -0
  32. data/app/views/context_drop/dashboard/feedbacks/index.html.erb +86 -0
  33. data/app/views/context_drop/dashboard/feedbacks/show.html.erb +121 -0
  34. data/app/views/layouts/context_drop/application.html.erb +215 -0
  35. data/config/routes.rb +17 -0
  36. data/db/migrate/20261109000001_create_context_drop_feedbacks.rb +23 -0
  37. data/db/migrate/20261109000002_create_context_drop_element_references.rb +29 -0
  38. data/db/migrate/20261109000003_create_context_drop_attachments.rb +24 -0
  39. data/lib/context_drop/base64_util.rb +15 -0
  40. data/lib/context_drop/configuration.rb +155 -0
  41. data/lib/context_drop/engine.rb +32 -0
  42. data/lib/context_drop/mcp/dispatcher.rb +197 -0
  43. data/lib/context_drop/mcp/prompts/investigate_feedback.rb +83 -0
  44. data/lib/context_drop/mcp/server.rb +25 -0
  45. data/lib/context_drop/mcp/stdio_runner.rb +62 -0
  46. data/lib/context_drop/mcp/tools/get_feedback.rb +133 -0
  47. data/lib/context_drop/mcp/tools/get_feedback_attachment.rb +71 -0
  48. data/lib/context_drop/mcp/tools/list_feedbacks.rb +127 -0
  49. data/lib/context_drop/mcp/tools/update_feedback_status.rb +72 -0
  50. data/lib/context_drop/version.rb +5 -0
  51. data/lib/context_drop.rb +32 -0
  52. data/lib/generators/context_drop/install/install_generator.rb +52 -0
  53. data/lib/generators/context_drop/install/templates/initializer.rb.tt +55 -0
  54. data/lib/tasks/context_drop_tasks.rake +14 -0
  55. metadata +246 -0
@@ -0,0 +1,3508 @@
1
+ /**
2
+ * ContextDrop Browser Widget Bundle (v0.1.0)
3
+ * Self-contained Custom Element (<context-drop-widget>) with Shadow DOM isolation.
4
+ * Generated from app/javascript/context_drop/*.ts — DO NOT EDIT DIRECTLY.
5
+ */
6
+ (function () {
7
+ "use strict";
8
+
9
+ // --- Module: sanitizer.ts ---
10
+ const MANDATORY_EXCLUDED_SELECTORS = [
11
+ "[data-feedback-ignore]",
12
+ "[data-feedback-sensitive]",
13
+ "input[type='password']",
14
+ "input[type='hidden']",
15
+ "[autocomplete^='cc-']",
16
+ "context-drop-widget",
17
+ "[data-context-drop-ui]"
18
+ ];
19
+ const ALLOWED_HTML_ATTRIBUTES = [
20
+ "id",
21
+ "class",
22
+ "role",
23
+ "name",
24
+ "type",
25
+ "href",
26
+ "title",
27
+ "placeholder",
28
+ "alt",
29
+ "data-testid",
30
+ "data-test",
31
+ "data-cy",
32
+ "data-qa",
33
+ "data-turbo-frame"
34
+ ];
35
+
36
+ const IGNORED_TEXT_TAGS = new Set([
37
+ "SCRIPT",
38
+ "STYLE",
39
+ "NOSCRIPT",
40
+ "TEMPLATE",
41
+ "SVG",
42
+ "PATH",
43
+ "INPUT",
44
+ "TEXTAREA",
45
+ "SELECT"
46
+ ]);
47
+ function buildExcludedSelectorList(customSelectors = []) {
48
+ const combined = new Set (MANDATORY_EXCLUDED_SELECTORS);
49
+ for (const sel of customSelectors) {
50
+ if (typeof sel === "string" && sel.trim().length > 0) {
51
+ combined.add(sel.trim());
52
+ }
53
+ }
54
+ return Array.from(combined);
55
+ }
56
+
57
+ function elementMatchesAny(el , selectors ) {
58
+ const tagName = (el.tagName || "").toLowerCase();
59
+ if (tagName === "context-drop-widget") return true;
60
+ if (el.hasAttribute("data-feedback-ignore") || el.hasAttribute("data-feedback-sensitive")) {
61
+ return true;
62
+ }
63
+ if (el.hasAttribute("data-context-drop-ui")) {
64
+ return true;
65
+ }
66
+ if (tagName === "input") {
67
+ const inputType = (el.getAttribute("type") || "").toLowerCase();
68
+ if (inputType === "password" || inputType === "hidden") return true;
69
+ }
70
+ const autocomplete = (el.getAttribute("autocomplete") || "").toLowerCase();
71
+ if (autocomplete.startsWith("cc-")) return true;
72
+
73
+ if (typeof el.matches === "function") {
74
+ for (const sel of selectors) {
75
+ try {
76
+ if (el.matches(sel)) return true;
77
+ } catch {
78
+ // Ignore invalid custom CSS selector syntax
79
+ }
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+ function getParentOrHostElement(node ) {
85
+ if (!node) return null;
86
+ if (node.parentElement) {
87
+ return node.parentElement;
88
+ }
89
+ const rootNode = typeof node.getRootNode === "function" ? node.getRootNode() : null;
90
+ if (rootNode && "host" in rootNode && (rootNode ).host instanceof Element) {
91
+ return (rootNode ).host;
92
+ }
93
+ return null;
94
+ }
95
+ function isElementExcluded(node , customSelectors = []) {
96
+ if (!node) return true;
97
+ const selectors = buildExcludedSelectorList(customSelectors);
98
+
99
+ let current = node;
100
+ while (current) {
101
+ if (elementMatchesAny(current, selectors)) {
102
+ return true;
103
+ }
104
+ current = getParentOrHostElement(current);
105
+ }
106
+ return false;
107
+ }
108
+ function isSelectableElement(node , customSelectors = []) {
109
+ if (!node) return false;
110
+ const tag = (node.tagName || "").toUpperCase();
111
+ if (!tag || tag === "HTML" || tag === "BODY" || tag === "HEAD" || tag === "SCRIPT" || tag === "STYLE") {
112
+ return false;
113
+ }
114
+ if (typeof node.closest === "function") {
115
+ if (node.closest("context-drop-widget") || node.closest("[data-context-drop-ui]")) {
116
+ return false;
117
+ }
118
+ }
119
+ if (isElementExcluded(node, customSelectors)) {
120
+ return false;
121
+ }
122
+ const rect = typeof node.getBoundingClientRect === "function"
123
+ ? node.getBoundingClientRect()
124
+ : { width: 0, height: 0 };
125
+ if ((rect.width ?? 0) <= 0 || (rect.height ?? 0) <= 0) {
126
+ return false;
127
+ }
128
+ return true;
129
+ }
130
+ function extractSanitizedAttributes(el ) {
131
+ const snapshot = {};
132
+ const attrNames = typeof el.getAttributeNames === "function" ? el.getAttributeNames() : [];
133
+
134
+ for (const rawName of attrNames) {
135
+ const name = rawName.toLowerCase().trim();
136
+ if (!name || name === "value" || name === "defaultvalue" || name.startsWith("on")) {
137
+ continue;
138
+ }
139
+ if (name === "data-feedback-ignore" || name === "data-feedback-sensitive") {
140
+ continue;
141
+ }
142
+
143
+ const isAllowed =
144
+ ALLOWED_HTML_ATTRIBUTES.includes(name) ||
145
+ (name.startsWith("aria-") && name !== "aria-hidden");
146
+
147
+ if (!isAllowed) continue;
148
+
149
+ const rawVal = el.getAttribute(rawName);
150
+ if (rawVal === null) continue;
151
+
152
+ const trimmed = rawVal.trim();
153
+ if (name === "href" && /^(javascript|data):/i.test(trimmed)) {
154
+ continue;
155
+ }
156
+ snapshot[name] = trimmed.slice(0, 300);
157
+ }
158
+
159
+ return snapshot;
160
+ }
161
+ function extractSanitizedText(el , customSelectors = []) {
162
+ const tagName = (el.tagName || "").toUpperCase();
163
+ if (IGNORED_TEXT_TAGS.has(tagName)) {
164
+ return null;
165
+ }
166
+
167
+ const selectors = buildExcludedSelectorList(customSelectors);
168
+ const parts = [];
169
+
170
+ function walk(node ) {
171
+ if (node.nodeType === 3) {
172
+ // Node.TEXT_NODE === 3
173
+ const text = node.nodeValue || "";
174
+ if (text.trim().length > 0) {
175
+ parts.push(text.trim());
176
+ }
177
+ return;
178
+ }
179
+
180
+ if (node.nodeType === 1) {
181
+ // Node.ELEMENT_NODE === 1
182
+ const childEl = node ;
183
+ const childTag = (childEl.tagName || "").toUpperCase();
184
+ if (IGNORED_TEXT_TAGS.has(childTag)) return;
185
+ if (elementMatchesAny(childEl, selectors)) return;
186
+
187
+ const children = childEl.childNodes || [];
188
+ for (let i = 0; i < children.length; i += 1) {
189
+ walk(children[i]);
190
+ }
191
+ }
192
+ }
193
+
194
+ walk(el);
195
+
196
+ const joined = parts.join(" ").replace(/\s+/g, " ").trim();
197
+ if (joined.length === 0) return null;
198
+ return joined.slice(0, 500);
199
+ }
200
+
201
+ function escapeCssIdentifier(ident ) {
202
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
203
+ return CSS.escape(ident);
204
+ }
205
+ return ident.replace(/([^a-zA-Z0-9_-])/g, "\\$1");
206
+ }
207
+ function computeSelector(el ) {
208
+ const tag = (el.tagName || "div").toLowerCase();
209
+ const id = (el.getAttribute("id") || "").trim();
210
+ if (id) {
211
+ return `#${escapeCssIdentifier(id)}`;
212
+ }
213
+
214
+ const testId =
215
+ el.getAttribute("data-testid") ||
216
+ el.getAttribute("data-test") ||
217
+ el.getAttribute("data-cy") ||
218
+ el.getAttribute("data-qa");
219
+
220
+ if (testId && testId.trim()) {
221
+ const attrKey = el.hasAttribute("data-testid")
222
+ ? "data-testid"
223
+ : el.hasAttribute("data-test")
224
+ ? "data-test"
225
+ : el.hasAttribute("data-cy")
226
+ ? "data-cy"
227
+ : "data-qa";
228
+ return `${tag}[${attrKey}="${testId.trim().replace(/"/g, '\\"')}"]`;
229
+ }
230
+
231
+ const classAttr = (el.getAttribute("class") || "").trim();
232
+ if (classAttr) {
233
+ const classes = classAttr
234
+ .split(/\s+/)
235
+ .filter((c) => c.length > 0 && !c.startsWith("cd-"))
236
+ .slice(0, 3)
237
+ .map((c) => `.${escapeCssIdentifier(c)}`)
238
+ .join("");
239
+ if (classes) {
240
+ return `${tag}${classes}`;
241
+ }
242
+ }
243
+
244
+ return tag;
245
+ }
246
+ function computeDomPath(el ) {
247
+ const segments = [];
248
+ let current = el;
249
+
250
+ while (current && segments.length < 15) {
251
+ const tag = (current.tagName || "").toLowerCase();
252
+ if (!tag) break;
253
+
254
+ let segment = tag;
255
+ const id = (current.getAttribute("id") || "").trim();
256
+ if (id) {
257
+ segment += `#${id}`;
258
+ } else {
259
+ const classAttr = (current.getAttribute("class") || "").trim();
260
+ if (classAttr) {
261
+ const firstClasses = classAttr
262
+ .split(/\s+/)
263
+ .filter(Boolean)
264
+ .slice(0, 2)
265
+ .join(".");
266
+ if (firstClasses) {
267
+ segment += `.${firstClasses}`;
268
+ }
269
+ }
270
+ }
271
+
272
+ segments.unshift(segment);
273
+ current = getParentOrHostElement(current);
274
+ }
275
+
276
+ return segments.join(" > ").slice(0, 1000);
277
+ }
278
+ function computeBoundingRect(el ) {
279
+ const rect = typeof el.getBoundingClientRect === "function"
280
+ ? el.getBoundingClientRect()
281
+ : { x: 0, y: 0, width: 0, height: 0 };
282
+
283
+ return {
284
+ x: Math.round((rect.x ?? (rect ).left ?? 0) * 100) / 100,
285
+ y: Math.round((rect.y ?? (rect ).top ?? 0) * 100) / 100,
286
+ width: Math.round((rect.width ?? 0) * 100) / 100,
287
+ height: Math.round((rect.height ?? 0) * 100) / 100
288
+ };
289
+ }
290
+ function describeElementCandidate(
291
+ el ,
292
+ relation ,
293
+ depth ,
294
+ customSelectors = []
295
+ ) {
296
+ const rawText = extractSanitizedText(el, customSelectors);
297
+ const textPreview = rawText
298
+ ? rawText.length > 64
299
+ ? `${rawText.slice(0, 61)}...`
300
+ : rawText
301
+ : null;
302
+
303
+ return {
304
+ element: el,
305
+ tagName: (el.tagName || "div").toLowerCase(),
306
+ selector: computeSelector(el),
307
+ domPath: computeDomPath(el),
308
+ textPreview,
309
+ rect: computeBoundingRect(el),
310
+ relation,
311
+ depth
312
+ };
313
+ }
314
+ function resolveAncestorChain(
315
+ el ,
316
+ customSelectors = [],
317
+ maxDepth = 8
318
+ ) {
319
+ if (!el || !isSelectableElement(el, customSelectors)) {
320
+ return [];
321
+ }
322
+
323
+ const chain = [];
324
+ let current = el;
325
+
326
+ while (current && chain.length < maxDepth) {
327
+ if (isSelectableElement(current, customSelectors)) {
328
+ chain.unshift(current);
329
+ }
330
+ current = getParentOrHostElement(current);
331
+ }
332
+
333
+ return chain;
334
+ }
335
+
336
+ function isAncestorOf(possibleAncestor , node ) {
337
+ let current = getParentOrHostElement(node);
338
+ while (current) {
339
+ if (current === possibleAncestor) return true;
340
+ current = getParentOrHostElement(current);
341
+ }
342
+ return false;
343
+ }
344
+
345
+ /**
346
+ * Builds an ordered list of candidate elements for a pointer location so users can
347
+ * disambiguate overlapping sibling layers, deeply nested children, and parent containers.
348
+ *
349
+ * Order:
350
+ * 1. Direct child elements (if the target is a container with immediate selectable children)
351
+ * 2. Primary hovered/clicked target ("target")
352
+ * 3. Overlapping non-ancestor elements at the same screen coordinate ("overlap")
353
+ * 4. DOM ancestors from immediate parent up to top-level layout section ("ancestor")
354
+ */
355
+ function resolveCandidateStack(
356
+ primaryTarget ,
357
+ pointElements = [],
358
+ customSelectors = [],
359
+ maxCandidates = 10
360
+ ) {
361
+ const seen = new Set ();
362
+ const stack = [];
363
+
364
+ const validPrimary =
365
+ primaryTarget && isSelectableElement(primaryTarget, customSelectors)
366
+ ? primaryTarget
367
+ : pointElements.find((candidate) => isSelectableElement(candidate, customSelectors)) || null;
368
+
369
+ if (!validPrimary) {
370
+ return [];
371
+ }
372
+
373
+ // 1. Primary target at index 0 so default hover/click is the exact element under pointer
374
+ seen.add(validPrimary);
375
+ stack.push(describeElementCandidate(validPrimary, "target", 0, customSelectors));
376
+
377
+ // 2. Overlapping non-ancestor elements from elementsFromPoint (e.g., elements behind an overlay or badge)
378
+ for (const ptEl of pointElements) {
379
+ if (stack.length >= maxCandidates) break;
380
+ if (seen.has(ptEl)) continue;
381
+ if (!isSelectableElement(ptEl, customSelectors)) continue;
382
+ if (!isAncestorOf(ptEl, validPrimary)) {
383
+ seen.add(ptEl);
384
+ stack.push(describeElementCandidate(ptEl, "overlap", stack.length, customSelectors));
385
+ }
386
+ }
387
+
388
+ // 3. Direct selectable children of the primary target (when a wrapper intercepts hover)
389
+ const childNodes = (validPrimary ).children ||
390
+ (validPrimary.childNodes || []);
391
+ for (let i = 0; i < childNodes.length; i += 1) {
392
+ if (stack.length >= maxCandidates) break;
393
+ const node = childNodes[i] ;
394
+ if (node && node.nodeType === 1 && !seen.has(node) && isSelectableElement(node, customSelectors)) {
395
+ seen.add(node);
396
+ stack.push(describeElementCandidate(node, "child", stack.length, customSelectors));
397
+ }
398
+ }
399
+
400
+ // 4. Ancestor chain walking up from validPrimary
401
+ let parent = getParentOrHostElement(validPrimary);
402
+ while (parent && stack.length < maxCandidates) {
403
+ if (!seen.has(parent) && isSelectableElement(parent, customSelectors)) {
404
+ seen.add(parent);
405
+ stack.push(describeElementCandidate(parent, "ancestor", stack.length, customSelectors));
406
+ }
407
+ parent = getParentOrHostElement(parent);
408
+ }
409
+
410
+ return stack;
411
+ }
412
+ function captureElementSnapshot(
413
+ el ,
414
+ customSelectors = [],
415
+ clientRef
416
+ ) {
417
+ if (isElementExcluded(el, customSelectors)) {
418
+ return null;
419
+ }
420
+
421
+ const boundingRect = computeBoundingRect(el);
422
+
423
+ const viewport = {
424
+ width: typeof window !== "undefined" ? window.innerWidth || 1024 : 1024,
425
+ height: typeof window !== "undefined" ? window.innerHeight || 768 : 768
426
+ };
427
+
428
+ const generatedRef = clientRef || `el_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
429
+
430
+ return {
431
+ client_ref: generatedRef,
432
+ tag_name: (el.tagName || "div").toLowerCase(),
433
+ selector: computeSelector(el),
434
+ dom_path: computeDomPath(el),
435
+ text_content: extractSanitizedText(el, customSelectors),
436
+ attributes_snapshot: extractSanitizedAttributes(el),
437
+ bounding_rect: boundingRect,
438
+ viewport: viewport,
439
+ metadata: {},
440
+ captured_at: new Date().toISOString()
441
+ };
442
+ }
443
+
444
+ // --- Module: icons.ts ---
445
+ /**
446
+ * Authored inline SVG icon set for ContextDrop.
447
+ * All icons share a 16x16 or 20x20 viewBox, 1.75px stroke weight, and round caps/joins.
448
+ */
449
+ const ICON_PIN = `<svg viewBox="0 0 20 20" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11.8 3.2l5 5-2.1 2.1-1.8-.6-3.2 3.2.6 2.5-1.6 1.6-3.1-3.1-3.1 3.1v-.1l3.1-3.1-3.1-3.1 1.6-1.6 2.5.6 3.2-3.2-.6-1.8 2.1-2.1z"/></svg>`;
450
+ const ICON_PIN_DROP = `<svg viewBox="0 0 20 20" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 2.5a4.5 4.5 0 0 1 4.5 4.5c0 3.1-4.5 7.5-4.5 7.5S5.5 10.1 5.5 7A4.5 4.5 0 0 1 10 2.5z"/><circle cx="10" cy="7" r="1.6"/><path d="M5 17.5h10"/></svg>`;
451
+ const ICON_LAYERS = `<svg viewBox="0 0 20 20" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="10 2.5 17.5 6.5 10 10.5 2.5 6.5 10 2.5"/><polyline points="2.5 10.2 10 14.2 17.5 10.2"/><polyline points="2.5 13.8 10 17.8 17.5 13.8"/></svg>`;
452
+ const ICON_CHEVRON_UP = `<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3.5 10 8 5.5 12.5 10"/></svg>`;
453
+ const ICON_CHEVRON_DOWN = `<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3.5 6 8 10.5 12.5 6"/></svg>`;
454
+ const ICON_CHEVRON_RIGHT = `<svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 3.5 10.5 8 6 12.5"/></svg>`;
455
+ const ICON_CAMERA = `<svg viewBox="0 0 20 20" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6.5h2.5l1.4-2h6.2l1.4 2H17a1.5 1.5 0 0 1 1.5 1.5v7.5A1.5 1.5 0 0 1 17 17H3a1.5 1.5 0 0 1-1.5-1.5V8A1.5 1.5 0 0 1 3 6.5z"/><circle cx="10" cy="11.5" r="2.75"/></svg>`;
456
+ const ICON_IMAGE = `<svg viewBox="0 0 20 20" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="3.5" width="15" height="13" rx="2"/><circle cx="7" cy="8" r="1.3"/><path d="M17.5 13.2l-3.8-3.7L6 16.5"/></svg>`;
457
+ const ICON_SEND = `<svg viewBox="0 0 20 20" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 16V4"/><polyline points="5 9 10 4 15 9"/></svg>`;
458
+ const ICON_CLOSE = `<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="4" y1="4" x2="12" y2="12"/><line x1="12" y1="4" x2="4" y2="12"/></svg>`;
459
+ const ICON_MINIMIZE = `<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3.5" y1="8" x2="12.5" y2="8"/></svg>`;
460
+ const ICON_EXPAND = `<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 3.5 12.5 3.5 12.5 10"/><line x1="12.5" y1="3.5" x2="3.5" y2="12.5"/></svg>`;
461
+ const ICON_GRIP = `<svg viewBox="0 0 12 16" width="10" height="14" fill="currentColor" aria-hidden="true"><circle cx="3.5" cy="4" r="1.1"/><circle cx="8.5" cy="4" r="1.1"/><circle cx="3.5" cy="8" r="1.1"/><circle cx="8.5" cy="8" r="1.1"/><circle cx="3.5" cy="12" r="1.1"/><circle cx="8.5" cy="12" r="1.1"/></svg>`;
462
+ const ICON_CHECK = `<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3.5 8.5 6.5 11.5 12.5 4.5"/></svg>`;
463
+ const ICON_CHAT = `<svg viewBox="0 0 20 20" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H8l-4 3v-3H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/></svg>`;
464
+
465
+ // --- Module: styles.ts ---
466
+ const WIDGET_SHADOW_STYLES = `
467
+ :host {
468
+ all: initial;
469
+ --cd-ink: #111318;
470
+ --cd-ink-secondary: #4b5262;
471
+ --cd-ink-subtle: #5c6478;
472
+ --cd-surface: #faf9f6;
473
+ --cd-surface-recessed: #f1efea;
474
+ --cd-surface-elevated: #ffffff;
475
+ --cd-border: #d6d3cd;
476
+ --cd-border-subtle: #e4e1da;
477
+ --cd-pin: #c2410c;
478
+ --cd-pin-hover: #9a3412;
479
+ --cd-pin-soft: #ffedd5;
480
+ --cd-accent: #1d4ed8;
481
+ --cd-accent-hover: #1e40af;
482
+ --cd-accent-soft: #dbeafe;
483
+ --cd-danger: #b91c1c;
484
+ --cd-danger-soft: #fee2e2;
485
+ --cd-success: #15803d;
486
+ --cd-success-soft: #dcfce7;
487
+ --cd-shadow-float: 0 22px 44px -12px rgba(17, 19, 24, 0.22), 0 8px 18px -6px rgba(17, 19, 24, 0.10);
488
+ --cd-shadow-pin: 0 12px 24px -6px rgba(17, 19, 24, 0.24), 0 4px 10px -3px rgba(17, 19, 24, 0.12);
489
+ --cd-radius-lg: 16px;
490
+ --cd-radius-md: 10px;
491
+ --cd-radius-sm: 6px;
492
+ --cd-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
493
+ --cd-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
494
+ --cd-ease: cubic-bezier(0.22, 1, 0.36, 1);
495
+ font-family: var(--cd-font);
496
+ font-size: 13.5px;
497
+ line-height: 1.5;
498
+ color: var(--cd-ink);
499
+ caret-color: var(--cd-pin);
500
+ z-index: 2147483640;
501
+ }
502
+
503
+ *, *::before, *::after {
504
+ box-sizing: border-box;
505
+ }
506
+
507
+ ::selection {
508
+ background: var(--cd-pin-soft);
509
+ color: var(--cd-ink);
510
+ }
511
+
512
+ .cd-root {
513
+ position: fixed;
514
+ z-index: 2147483640;
515
+ display: flex;
516
+ flex-direction: column;
517
+ pointer-events: none;
518
+ max-width: calc(100vw - 24px);
519
+ }
520
+
521
+ .cd-root.pos-top_left {
522
+ top: 20px;
523
+ left: 20px;
524
+ align-items: flex-start;
525
+ }
526
+
527
+ .cd-root.pos-top_right {
528
+ top: 20px;
529
+ right: 20px;
530
+ align-items: flex-end;
531
+ }
532
+
533
+ .cd-root.pos-bottom_left {
534
+ bottom: 20px;
535
+ left: 20px;
536
+ align-items: flex-start;
537
+ }
538
+
539
+ .cd-root.pos-bottom_right {
540
+ bottom: 20px;
541
+ right: 20px;
542
+ align-items: flex-end;
543
+ }
544
+
545
+ .cd-root.is-free-positioned {
546
+ bottom: auto;
547
+ right: auto;
548
+ align-items: flex-start;
549
+ }
550
+
551
+ /* Focus states across all interactive controls */
552
+ button:focus-visible,
553
+ textarea:focus-visible {
554
+ outline: 2px solid var(--cd-accent);
555
+ outline-offset: 2px;
556
+ }
557
+
558
+ /* Custom themed scrollbars */
559
+ .cd-chat-thread::-webkit-scrollbar,
560
+ .cd-tray::-webkit-scrollbar,
561
+ .cd-breadcrumb-scroll::-webkit-scrollbar {
562
+ width: 6px;
563
+ height: 6px;
564
+ }
565
+
566
+ .cd-chat-thread::-webkit-scrollbar-track,
567
+ .cd-tray::-webkit-scrollbar-track,
568
+ .cd-breadcrumb-scroll::-webkit-scrollbar-track {
569
+ background: transparent;
570
+ }
571
+
572
+ .cd-chat-thread::-webkit-scrollbar-thumb,
573
+ .cd-tray::-webkit-scrollbar-thumb,
574
+ .cd-breadcrumb-scroll::-webkit-scrollbar-thumb {
575
+ background: var(--cd-border);
576
+ border-radius: 9999px;
577
+ }
578
+
579
+ /* Freely Floating Pin Trigger Dock */
580
+ .cd-pin-dock {
581
+ pointer-events: auto;
582
+ display: inline-flex;
583
+ align-items: center;
584
+ gap: 2px;
585
+ padding: 4px;
586
+ background: var(--cd-ink);
587
+ color: var(--cd-surface);
588
+ border: 1px solid rgba(250, 249, 246, 0.14);
589
+ border-radius: 9999px 9999px 9999px 6px;
590
+ box-shadow: var(--cd-shadow-pin);
591
+ user-select: none;
592
+ touch-action: none;
593
+ transition: transform 0.18s var(--cd-ease), box-shadow 0.18s var(--cd-ease);
594
+ }
595
+
596
+ .cd-pin-dock[hidden] {
597
+ display: none !important;
598
+ }
599
+
600
+ .cd-pin-dock.is-dragging {
601
+ cursor: grabbing;
602
+ transform: scale(1.03);
603
+ }
604
+
605
+ .cd-pin-dock-grip {
606
+ display: inline-flex;
607
+ align-items: center;
608
+ justify-content: center;
609
+ padding: 4px 3px 4px 5px;
610
+ color: #9ca3b5;
611
+ cursor: grab;
612
+ }
613
+
614
+ .cd-pin-dock-grip:active {
615
+ cursor: grabbing;
616
+ }
617
+
618
+ .cd-floating-pin-btn {
619
+ appearance: none;
620
+ display: inline-flex;
621
+ align-items: center;
622
+ gap: 6px;
623
+ padding: 7px 11px;
624
+ border-radius: 9999px 9999px 9999px 4px;
625
+ border: 0;
626
+ background: var(--cd-pin);
627
+ color: var(--cd-surface);
628
+ font-family: var(--cd-font);
629
+ font-size: 12.5px;
630
+ font-weight: 600;
631
+ cursor: pointer;
632
+ transition: background-color 0.15s var(--cd-ease), transform 0.15s var(--cd-ease);
633
+ }
634
+
635
+ .cd-floating-pin-btn:hover {
636
+ background: var(--cd-pin-hover);
637
+ }
638
+
639
+ .cd-floating-pin-btn.is-active {
640
+ background: var(--cd-accent);
641
+ }
642
+
643
+ .cd-trigger {
644
+ appearance: none;
645
+ display: inline-flex;
646
+ align-items: center;
647
+ gap: 6px;
648
+ padding: 7px 12px;
649
+ border-radius: 9999px;
650
+ border: 0;
651
+ background: transparent;
652
+ color: var(--cd-surface);
653
+ font-family: var(--cd-font);
654
+ font-size: 12.5px;
655
+ font-weight: 600;
656
+ cursor: pointer;
657
+ transition: background-color 0.15s var(--cd-ease);
658
+ }
659
+
660
+ .cd-trigger:hover {
661
+ background: rgba(250, 249, 246, 0.12);
662
+ }
663
+
664
+ .cd-count-pill {
665
+ display: inline-flex;
666
+ align-items: center;
667
+ justify-content: center;
668
+ min-width: 18px;
669
+ height: 18px;
670
+ padding: 0 5px;
671
+ border-radius: 9999px;
672
+ background: var(--cd-pin-soft);
673
+ color: var(--cd-pin-hover);
674
+ font-size: 11px;
675
+ font-weight: 700;
676
+ font-variant-numeric: tabular-nums;
677
+ }
678
+
679
+ /* Compact Minimized Bar (active while pinning elements or minimized) */
680
+ .cd-compact-bar {
681
+ pointer-events: auto;
682
+ display: flex;
683
+ align-items: center;
684
+ gap: 8px;
685
+ width: 440px;
686
+ max-width: calc(100vw - 24px);
687
+ padding: 7px 10px;
688
+ background: var(--cd-surface);
689
+ color: var(--cd-ink);
690
+ border: 1px solid var(--cd-border);
691
+ border-radius: 14px;
692
+ box-shadow: var(--cd-shadow-float);
693
+ margin-bottom: 10px;
694
+ user-select: none;
695
+ touch-action: none;
696
+ }
697
+
698
+ .cd-compact-bar[hidden] {
699
+ display: none !important;
700
+ }
701
+
702
+ .cd-compact-status {
703
+ display: flex;
704
+ align-items: center;
705
+ gap: 6px;
706
+ flex: 1;
707
+ min-width: 0;
708
+ }
709
+
710
+ .cd-breadcrumb-scroll {
711
+ display: flex;
712
+ align-items: center;
713
+ gap: 3px;
714
+ overflow-x: auto;
715
+ white-space: nowrap;
716
+ scrollbar-width: thin;
717
+ flex: 1;
718
+ min-width: 0;
719
+ padding: 2px 0;
720
+ }
721
+
722
+ .cd-crumb-btn {
723
+ appearance: none;
724
+ border: 1px solid transparent;
725
+ background: var(--cd-surface-recessed);
726
+ color: var(--cd-ink-secondary);
727
+ font-family: var(--cd-mono);
728
+ font-size: 11px;
729
+ padding: 2px 6px;
730
+ border-radius: 5px;
731
+ cursor: pointer;
732
+ flex-shrink: 0;
733
+ }
734
+
735
+ .cd-crumb-btn:hover {
736
+ border-color: var(--cd-accent);
737
+ color: var(--cd-ink);
738
+ }
739
+
740
+ .cd-crumb-btn.is-current {
741
+ background: var(--cd-accent-soft);
742
+ border-color: var(--cd-accent);
743
+ color: var(--cd-accent-hover);
744
+ font-weight: 600;
745
+ }
746
+
747
+ .cd-crumb-sep {
748
+ display: inline-flex;
749
+ color: var(--cd-ink-subtle);
750
+ flex-shrink: 0;
751
+ }
752
+
753
+ /* Main Floating Chatbox Composer Window */
754
+ .cd-composer {
755
+ pointer-events: auto;
756
+ width: 400px;
757
+ max-width: calc(100vw - 24px);
758
+ background: var(--cd-surface);
759
+ border: 1px solid var(--cd-border);
760
+ border-radius: var(--cd-radius-lg);
761
+ box-shadow: var(--cd-shadow-float);
762
+ margin-bottom: 10px;
763
+ overflow: hidden;
764
+ display: flex;
765
+ flex-direction: column;
766
+ transition: opacity 0.18s var(--cd-ease), transform 0.18s var(--cd-ease);
767
+ }
768
+
769
+ .cd-composer[hidden] {
770
+ display: none !important;
771
+ }
772
+
773
+ .cd-composer.is-dragging-over {
774
+ border-color: var(--cd-accent);
775
+ }
776
+
777
+ /* Chatbox Header with Top-Left Pin Selector & Drag Bar */
778
+ .cd-composer-header {
779
+ display: flex;
780
+ align-items: center;
781
+ justify-content: space-between;
782
+ gap: 8px;
783
+ padding: 9px 11px;
784
+ background: var(--cd-surface-recessed);
785
+ border-bottom: 1px solid var(--cd-border);
786
+ cursor: grab;
787
+ user-select: none;
788
+ touch-action: none;
789
+ }
790
+
791
+ .cd-composer-header:active {
792
+ cursor: grabbing;
793
+ }
794
+
795
+ .cd-header-left {
796
+ display: flex;
797
+ align-items: center;
798
+ gap: 8px;
799
+ min-width: 0;
800
+ }
801
+
802
+ /* Pin-Shaped Selector on the Top-Left Corner of the Chatbox */
803
+ .cd-pin-corner-btn {
804
+ appearance: none;
805
+ display: inline-flex;
806
+ align-items: center;
807
+ gap: 6px;
808
+ padding: 6px 10px;
809
+ border-radius: 11px 11px 11px 3px;
810
+ border: 1px solid rgba(194, 65, 12, 0.25);
811
+ background: var(--cd-pin);
812
+ color: var(--cd-surface);
813
+ font-family: var(--cd-font);
814
+ font-size: 12px;
815
+ font-weight: 600;
816
+ cursor: pointer;
817
+ box-shadow: 0 4px 10px -2px rgba(194, 65, 12, 0.28);
818
+ transition: background-color 0.15s var(--cd-ease), transform 0.15s var(--cd-ease);
819
+ }
820
+
821
+ .cd-pin-corner-btn:hover {
822
+ background: var(--cd-pin-hover);
823
+ transform: translateY(-1px);
824
+ }
825
+
826
+ .cd-pin-corner-btn.is-active {
827
+ background: var(--cd-accent);
828
+ border-color: var(--cd-accent);
829
+ box-shadow: 0 4px 10px -2px rgba(29, 78, 216, 0.30);
830
+ }
831
+
832
+ .cd-composer-title {
833
+ font-size: 13px;
834
+ font-weight: 600;
835
+ color: var(--cd-ink);
836
+ margin: 0;
837
+ white-space: nowrap;
838
+ overflow: hidden;
839
+ text-overflow: ellipsis;
840
+ }
841
+
842
+ .cd-header-actions {
843
+ display: flex;
844
+ align-items: center;
845
+ gap: 4px;
846
+ }
847
+
848
+ .cd-icon-btn {
849
+ appearance: none;
850
+ display: inline-flex;
851
+ align-items: center;
852
+ justify-content: center;
853
+ width: 28px;
854
+ height: 28px;
855
+ border: 1px solid transparent;
856
+ background: transparent;
857
+ color: var(--cd-ink-secondary);
858
+ border-radius: 7px;
859
+ cursor: pointer;
860
+ transition: background-color 0.12s ease, color 0.12s ease;
861
+ }
862
+
863
+ .cd-icon-btn:hover {
864
+ background: rgba(17, 19, 24, 0.06);
865
+ color: var(--cd-ink);
866
+ }
867
+
868
+ /* Conversational Chat Thread */
869
+ .cd-chat-thread {
870
+ display: flex;
871
+ flex-direction: column;
872
+ gap: 10px;
873
+ max-height: 230px;
874
+ overflow-y: auto;
875
+ padding: 12px 14px;
876
+ background: var(--cd-surface);
877
+ scrollbar-width: thin;
878
+ }
879
+
880
+ .cd-chat-empty {
881
+ display: flex;
882
+ flex-direction: column;
883
+ gap: 8px;
884
+ padding: 10px 12px;
885
+ background: var(--cd-surface-recessed);
886
+ border: 1px solid var(--cd-border-subtle);
887
+ border-radius: var(--cd-radius-md);
888
+ color: var(--cd-ink-secondary);
889
+ font-size: 12.5px;
890
+ }
891
+
892
+ .cd-chat-empty-text {
893
+ margin: 0;
894
+ color: var(--cd-ink);
895
+ font-weight: 500;
896
+ }
897
+
898
+ .cd-chat-empty-hint {
899
+ margin: 0;
900
+ font-size: 11.5px;
901
+ color: var(--cd-ink-secondary);
902
+ }
903
+
904
+ .cd-chat-msg {
905
+ display: flex;
906
+ flex-direction: column;
907
+ gap: 5px;
908
+ align-self: flex-end;
909
+ max-width: 92%;
910
+ padding: 9px 11px;
911
+ background: var(--cd-surface-elevated);
912
+ border: 1px solid var(--cd-border);
913
+ border-radius: 12px 12px 3px 12px;
914
+ }
915
+
916
+ .cd-chat-msg-text {
917
+ margin: 0;
918
+ font-size: 13px;
919
+ color: var(--cd-ink);
920
+ white-space: pre-wrap;
921
+ word-break: break-word;
922
+ }
923
+
924
+ .cd-chat-msg-meta {
925
+ display: flex;
926
+ align-items: center;
927
+ justify-content: space-between;
928
+ gap: 8px;
929
+ font-size: 11px;
930
+ color: var(--cd-ink-secondary);
931
+ font-variant-numeric: tabular-nums;
932
+ }
933
+
934
+ .cd-chat-msg-pills {
935
+ display: flex;
936
+ flex-wrap: wrap;
937
+ gap: 4px;
938
+ }
939
+
940
+ .cd-chat-msg-pill {
941
+ font-family: var(--cd-mono);
942
+ font-size: 10.5px;
943
+ padding: 1px 6px;
944
+ border-radius: 4px;
945
+ background: var(--cd-surface-recessed);
946
+ border: 1px solid var(--cd-border-subtle);
947
+ color: var(--cd-ink);
948
+ }
949
+
950
+ /* Pinned Elements & Attachments Context Tray */
951
+ .cd-tray {
952
+ display: flex;
953
+ flex-wrap: wrap;
954
+ gap: 6px;
955
+ max-height: 116px;
956
+ overflow-y: auto;
957
+ padding: 8px 12px;
958
+ background: var(--cd-surface-recessed);
959
+ border-top: 1px solid var(--cd-border-subtle);
960
+ scrollbar-width: thin;
961
+ }
962
+
963
+ .cd-tray:empty {
964
+ display: none;
965
+ }
966
+
967
+ .cd-chip {
968
+ display: inline-flex;
969
+ align-items: center;
970
+ gap: 5px;
971
+ max-width: 100%;
972
+ padding: 3px 7px;
973
+ background: var(--cd-surface-elevated);
974
+ border: 1px solid var(--cd-border);
975
+ border-radius: 7px;
976
+ font-size: 11.5px;
977
+ color: var(--cd-ink);
978
+ transition: border-color 0.12s ease;
979
+ }
980
+
981
+ .cd-chip[data-chip-type="element"]:hover {
982
+ border-color: var(--cd-pin);
983
+ }
984
+
985
+ .cd-chip-thumb {
986
+ width: 20px;
987
+ height: 20px;
988
+ border-radius: 4px;
989
+ object-fit: cover;
990
+ border: 1px solid var(--cd-border);
991
+ }
992
+
993
+ .cd-chip-badge {
994
+ display: inline-flex;
995
+ align-items: center;
996
+ gap: 3px;
997
+ font-size: 10px;
998
+ font-weight: 600;
999
+ padding: 1px 5px;
1000
+ border-radius: 4px;
1001
+ background: var(--cd-pin-soft);
1002
+ color: var(--cd-pin-hover);
1003
+ }
1004
+
1005
+ .cd-chip[data-chip-type="screenshot"] .cd-chip-badge,
1006
+ .cd-chip[data-chip-type="image"] .cd-chip-badge {
1007
+ background: var(--cd-accent-soft);
1008
+ color: var(--cd-accent-hover);
1009
+ }
1010
+
1011
+ .cd-chip-label {
1012
+ overflow: hidden;
1013
+ text-overflow: ellipsis;
1014
+ white-space: nowrap;
1015
+ max-width: 165px;
1016
+ font-family: var(--cd-mono);
1017
+ font-size: 11px;
1018
+ }
1019
+
1020
+ .cd-chip-action,
1021
+ .cd-chip-remove {
1022
+ appearance: none;
1023
+ display: inline-flex;
1024
+ align-items: center;
1025
+ justify-content: center;
1026
+ border: 0;
1027
+ background: transparent;
1028
+ color: var(--cd-ink-secondary);
1029
+ cursor: pointer;
1030
+ padding: 2px;
1031
+ border-radius: 4px;
1032
+ line-height: 1;
1033
+ }
1034
+
1035
+ .cd-chip-action:hover {
1036
+ color: var(--cd-accent);
1037
+ background: var(--cd-accent-soft);
1038
+ }
1039
+
1040
+ .cd-chip-remove:hover {
1041
+ color: var(--cd-danger);
1042
+ background: var(--cd-danger-soft);
1043
+ }
1044
+
1045
+ /* Status Banner */
1046
+ .cd-status {
1047
+ padding: 7px 12px;
1048
+ font-size: 12px;
1049
+ font-weight: 500;
1050
+ border-top: 1px solid var(--cd-border-subtle);
1051
+ }
1052
+
1053
+ .cd-status[hidden] {
1054
+ display: none !important;
1055
+ }
1056
+
1057
+ .cd-status.error {
1058
+ color: var(--cd-danger);
1059
+ background: var(--cd-danger-soft);
1060
+ }
1061
+
1062
+ .cd-status.success {
1063
+ color: var(--cd-success);
1064
+ background: var(--cd-success-soft);
1065
+ }
1066
+
1067
+ /* Chatbox Prompt Input Container */
1068
+ .cd-prompt-shell {
1069
+ display: flex;
1070
+ flex-direction: column;
1071
+ background: var(--cd-surface-elevated);
1072
+ border-top: 1px solid var(--cd-border);
1073
+ }
1074
+
1075
+ .cd-textarea {
1076
+ width: 100%;
1077
+ min-height: 68px;
1078
+ max-height: 180px;
1079
+ resize: vertical;
1080
+ border: 0;
1081
+ padding: 10px 13px 6px;
1082
+ font-family: var(--cd-font);
1083
+ font-size: 13.5px;
1084
+ line-height: 1.45;
1085
+ color: var(--cd-ink);
1086
+ background: transparent;
1087
+ }
1088
+
1089
+ .cd-textarea::placeholder {
1090
+ color: var(--cd-ink-secondary);
1091
+ }
1092
+
1093
+ .cd-toolbar {
1094
+ display: flex;
1095
+ align-items: center;
1096
+ justify-content: space-between;
1097
+ gap: 8px;
1098
+ padding: 6px 10px 9px;
1099
+ background: var(--cd-surface-elevated);
1100
+ }
1101
+
1102
+ .cd-toolbar-actions {
1103
+ display: flex;
1104
+ align-items: center;
1105
+ gap: 5px;
1106
+ flex-wrap: wrap;
1107
+ }
1108
+
1109
+ .cd-btn {
1110
+ appearance: none;
1111
+ display: inline-flex;
1112
+ align-items: center;
1113
+ gap: 5px;
1114
+ padding: 5px 9px;
1115
+ border-radius: 7px;
1116
+ border: 1px solid var(--cd-border);
1117
+ background: var(--cd-surface);
1118
+ color: var(--cd-ink);
1119
+ font-family: var(--cd-font);
1120
+ font-size: 12px;
1121
+ font-weight: 500;
1122
+ cursor: pointer;
1123
+ transition: background-color 0.12s ease, border-color 0.12s ease;
1124
+ }
1125
+
1126
+ .cd-btn:hover:not(:disabled) {
1127
+ background: var(--cd-surface-recessed);
1128
+ border-color: var(--cd-ink-secondary);
1129
+ }
1130
+
1131
+ .cd-btn:disabled {
1132
+ opacity: 0.5;
1133
+ cursor: not-allowed;
1134
+ }
1135
+
1136
+ .cd-btn-primary {
1137
+ background: var(--cd-ink);
1138
+ border-color: var(--cd-ink);
1139
+ color: var(--cd-surface);
1140
+ font-weight: 600;
1141
+ padding: 6px 12px;
1142
+ }
1143
+
1144
+ .cd-btn-primary:hover:not(:disabled) {
1145
+ background: #252a34;
1146
+ border-color: #252a34;
1147
+ }
1148
+
1149
+ @media (max-width: 480px) {
1150
+ .cd-composer,
1151
+ .cd-compact-bar {
1152
+ width: calc(100vw - 20px);
1153
+ }
1154
+ }
1155
+ `;
1156
+
1157
+ // --- Module: api-client.ts ---
1158
+ class ContextDropApiClient {
1159
+ baseUrl ;
1160
+
1161
+ constructor(baseUrl = "/context_drop") {
1162
+ this.baseUrl = baseUrl.replace(/\/+$/, "") || "/context_drop";
1163
+ }
1164
+
1165
+ getCsrfToken() {
1166
+ if (typeof document === "undefined") return null;
1167
+ const meta = document.querySelector('meta[name="csrf-token"]');
1168
+ return meta ? meta.getAttribute("content") : null;
1169
+ }
1170
+
1171
+ async fetchConfig() {
1172
+ const response = await fetch(`${this.baseUrl}/config`, {
1173
+ method: "GET",
1174
+ credentials: "same-origin",
1175
+ headers: {
1176
+ Accept: "application/json"
1177
+ }
1178
+ });
1179
+
1180
+ if (!response.ok) {
1181
+ throw new Error(`Failed to fetch ContextDrop config (${response.status})`);
1182
+ }
1183
+
1184
+ return (await response.json()) ;
1185
+ }
1186
+
1187
+ async uploadAttachment(file , filename ) {
1188
+ const formData = new FormData();
1189
+ formData.append("file", file, filename);
1190
+
1191
+ const headers = {
1192
+ Accept: "application/json"
1193
+ };
1194
+ const csrf = this.getCsrfToken();
1195
+ if (csrf) {
1196
+ headers["X-CSRF-Token"] = csrf;
1197
+ }
1198
+
1199
+ const response = await fetch(`${this.baseUrl}/attachments`, {
1200
+ method: "POST",
1201
+ credentials: "same-origin",
1202
+ headers,
1203
+ body: formData
1204
+ });
1205
+
1206
+ if (!response.ok) {
1207
+ const errBody = await response.json().catch(() => ({}));
1208
+ throw new Error((errBody ).error || `Upload failed (${response.status})`);
1209
+ }
1210
+
1211
+ return (await response.json()) ;
1212
+ }
1213
+
1214
+ async submitFeedback(payload ) {
1215
+ const headers = {
1216
+ "Content-Type": "application/json",
1217
+ Accept: "application/json"
1218
+ };
1219
+ const csrf = this.getCsrfToken();
1220
+ if (csrf) {
1221
+ headers["X-CSRF-Token"] = csrf;
1222
+ }
1223
+
1224
+ const response = await fetch(`${this.baseUrl}/feedback`, {
1225
+ method: "POST",
1226
+ credentials: "same-origin",
1227
+ headers,
1228
+ body: JSON.stringify({ feedback: payload })
1229
+ });
1230
+
1231
+ if (!response.ok) {
1232
+ const errBody = await response.json().catch(() => ({}));
1233
+ throw new Error((errBody ).error || `Submission failed (${response.status})`);
1234
+ }
1235
+
1236
+ return (await response.json()) ;
1237
+ }
1238
+ }
1239
+
1240
+ // --- Module: screenshot.ts ---
1241
+ function collectRedactionRects(customExcludedSelectors = []) {
1242
+ if (typeof document === "undefined") return [];
1243
+
1244
+ const selectors = buildExcludedSelectorList(customExcludedSelectors);
1245
+ const selectorQuery = [
1246
+ ...selectors.filter((s) => s !== "context-drop-widget" && s !== "[data-context-drop-ui]"),
1247
+ "iframe"
1248
+ ].join(", ");
1249
+
1250
+ const rects = [];
1251
+ let candidates ;
1252
+ try {
1253
+ candidates = document.querySelectorAll(selectorQuery);
1254
+ } catch {
1255
+ candidates = document.querySelectorAll(
1256
+ "[data-feedback-ignore], [data-feedback-sensitive], input[type='password'], input[type='hidden'], iframe"
1257
+ );
1258
+ }
1259
+
1260
+ for (let i = 0; i < candidates.length; i += 1) {
1261
+ const el = candidates[i];
1262
+ if (el.closest("context-drop-widget")) continue;
1263
+
1264
+ // For iframes, redact cross-origin or excluded iframes
1265
+ if (el.tagName.toLowerCase() === "iframe" && !isElementExcluded(el, customExcludedSelectors)) {
1266
+ const src = el.getAttribute("src") || "";
1267
+ const isCrossOrigin =
1268
+ src.startsWith("http://") ||
1269
+ src.startsWith("https://") ||
1270
+ src.startsWith("//");
1271
+ if (!isCrossOrigin) continue;
1272
+ }
1273
+
1274
+ const r = el.getBoundingClientRect();
1275
+ if (r.width > 0 && r.height > 0) {
1276
+ rects.push({
1277
+ x: r.x ?? r.left,
1278
+ y: r.y ?? r.top,
1279
+ width: r.width,
1280
+ height: r.height
1281
+ });
1282
+ }
1283
+ }
1284
+
1285
+ return rects;
1286
+ }
1287
+
1288
+ function dataUrlToBlob(dataUrl ) {
1289
+ const parts = dataUrl.split(";base64,");
1290
+ const mime = (parts[0] || "data:image/png").replace("data:", "");
1291
+ const b64 = parts[1] || "";
1292
+ const binary = typeof atob === "function" ? atob(b64) : "";
1293
+ const bytes = new Uint8Array(binary.length);
1294
+ for (let i = 0; i < binary.length; i += 1) {
1295
+ bytes[i] = binary.charCodeAt(i);
1296
+ }
1297
+ return new Blob([bytes], { type: mime });
1298
+ }
1299
+ async function captureViewportScreenshot(
1300
+ customExcludedSelectors = [],
1301
+ cropRegion
1302
+ ) {
1303
+ if (typeof document === "undefined" || typeof window === "undefined") {
1304
+ throw new Error("Screenshot capture is only available in a browser environment.");
1305
+ }
1306
+
1307
+ const widgets = Array.from(document.querySelectorAll("context-drop-widget, [data-context-drop-ui]")) ;
1308
+ const previousVisibility = widgets.map((el) => el.style.visibility);
1309
+
1310
+ try {
1311
+ // 1. Hide ContextDrop's own UI during capture
1312
+ for (const w of widgets) {
1313
+ w.style.visibility = "hidden";
1314
+ }
1315
+
1316
+ const vw = Math.max(window.innerWidth || 1024, 1);
1317
+ const vh = Math.max(window.innerHeight || 768, 1);
1318
+ const region = cropRegion
1319
+ ? {
1320
+ x: Math.max(0, Math.min(cropRegion.x, vw - 1)),
1321
+ y: Math.max(0, Math.min(cropRegion.y, vh - 1)),
1322
+ width: Math.max(1, Math.min(cropRegion.width, vw - Math.max(0, cropRegion.x))),
1323
+ height: Math.max(1, Math.min(cropRegion.height, vh - Math.max(0, cropRegion.y)))
1324
+ }
1325
+ : { x: 0, y: 0, width: vw, height: vh };
1326
+
1327
+ const redactionRects = collectRedactionRects(customExcludedSelectors);
1328
+
1329
+ const canvas = document.createElement("canvas");
1330
+ canvas.width = Math.round(region.width);
1331
+ canvas.height = Math.round(region.height);
1332
+ const ctx = canvas.getContext("2d");
1333
+
1334
+ if (!ctx) {
1335
+ throw new Error("Canvas 2D context is unavailable or restricted by browser policy.");
1336
+ }
1337
+
1338
+ // Paint page background
1339
+ const bodyBg =
1340
+ window.getComputedStyle && document.body
1341
+ ? window.getComputedStyle(document.body).backgroundColor
1342
+ : "#ffffff";
1343
+ ctx.fillStyle = bodyBg && bodyBg !== "rgba(0, 0, 0, 0)" ? bodyBg : "#ffffff";
1344
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1345
+
1346
+ // Paint visible non-excluded DOM elements approximation onto canvas
1347
+ const visibleNodes = document.body ? Array.from(document.body.querySelectorAll("*")) : [];
1348
+ for (const node of visibleNodes) {
1349
+ if (node.closest("context-drop-widget") || node.hasAttribute("data-context-drop-ui")) continue;
1350
+ if (isElementExcluded(node, customExcludedSelectors)) continue;
1351
+
1352
+ const r = node.getBoundingClientRect();
1353
+ if (r.width <= 0 || r.height <= 0) continue;
1354
+
1355
+ const rx = (r.x ?? r.left) - region.x;
1356
+ const ry = (r.y ?? r.top) - region.y;
1357
+ if (rx + r.width < 0 || ry + r.height < 0 || rx > region.width || ry > region.height) {
1358
+ continue;
1359
+ }
1360
+
1361
+ const style = window.getComputedStyle ? window.getComputedStyle(node) : null;
1362
+ if (style) {
1363
+ if (style.backgroundColor && style.backgroundColor !== "rgba(0, 0, 0, 0)" && style.backgroundColor !== "transparent") {
1364
+ ctx.fillStyle = style.backgroundColor;
1365
+ ctx.fillRect(rx, ry, r.width, r.height);
1366
+ }
1367
+ if (style.borderWidth && parseFloat(style.borderWidth) > 0) {
1368
+ ctx.strokeStyle = style.borderColor || "#cbd5e1";
1369
+ ctx.lineWidth = Math.min(parseFloat(style.borderWidth), 4);
1370
+ ctx.strokeRect(rx, ry, r.width, r.height);
1371
+ }
1372
+ }
1373
+ }
1374
+
1375
+ // 2. Enforce pre-capture visual redaction of all sensitive/excluded elements
1376
+ for (const red of redactionRects) {
1377
+ const rx = red.x - region.x;
1378
+ const ry = red.y - region.y;
1379
+ if (rx + red.width < 0 || ry + red.height < 0 || rx > region.width || ry > region.height) {
1380
+ continue;
1381
+ }
1382
+ ctx.fillStyle = "#0f172a";
1383
+ ctx.fillRect(rx, ry, red.width, red.height);
1384
+ ctx.fillStyle = "#94a3b8";
1385
+ ctx.font = "11px sans-serif";
1386
+ if (red.width >= 60 && red.height >= 16) {
1387
+ ctx.fillText("[REDACTED]", rx + 6, ry + Math.min(red.height - 4, 14));
1388
+ }
1389
+ }
1390
+
1391
+ const dataUrl = canvas.toDataURL("image/png");
1392
+ const blob = dataUrlToBlob(dataUrl);
1393
+
1394
+ return {
1395
+ dataUrl,
1396
+ blob,
1397
+ width: canvas.width,
1398
+ height: canvas.height,
1399
+ redactedCount: redactionRects.length,
1400
+ metadata: {
1401
+ capture_type: cropRegion ? "element" : "viewport",
1402
+ image_dimensions: { width: canvas.width, height: canvas.height },
1403
+ viewport: { width: vw, height: vh },
1404
+ redacted_elements_count: redactionRects.length,
1405
+ captured_at: new Date().toISOString()
1406
+ }
1407
+ };
1408
+ } finally {
1409
+ // Always restore ContextDrop UI visibility
1410
+ widgets.forEach((el, idx) => {
1411
+ el.style.visibility = previousVisibility[idx] || "";
1412
+ });
1413
+ }
1414
+ }
1415
+ async function captureElementScreenshot(
1416
+ element ,
1417
+ padding = 16,
1418
+ customExcludedSelectors = [],
1419
+ elementClientRef
1420
+ ) {
1421
+ if (isElementExcluded(element, customExcludedSelectors)) {
1422
+ throw new Error("Cannot capture screenshot of an excluded or sensitive DOM element.");
1423
+ }
1424
+
1425
+ const r = element.getBoundingClientRect();
1426
+ const pad = Math.max(0, padding);
1427
+ const cropRegion = {
1428
+ x: (r.x ?? r.left) - pad,
1429
+ y: (r.y ?? r.top) - pad,
1430
+ width: r.width + pad * 2,
1431
+ height: r.height + pad * 2
1432
+ };
1433
+
1434
+ const result = await captureViewportScreenshot(customExcludedSelectors, cropRegion);
1435
+ result.metadata.capture_type = "element";
1436
+ if (elementClientRef) {
1437
+ result.metadata.element_attachment_ref = elementClientRef;
1438
+ }
1439
+ return result;
1440
+ }
1441
+
1442
+ // --- Module: element-picker.ts ---
1443
+ class ElementPicker {
1444
+ active = false;
1445
+ anchored = false;
1446
+ anchorX = 0;
1447
+ anchorY = 0;
1448
+ lastPointerX = 0;
1449
+ lastPointerY = 0;
1450
+ wheelAccumulator = 0;
1451
+
1452
+ callbacks ;
1453
+ overlayEl = null;
1454
+ leafOutlineEl = null;
1455
+ labelEl = null;
1456
+ pinMarkerEl = null;
1457
+ popoverEl = null;
1458
+ previewOverlayEl = null;
1459
+
1460
+ candidateStack = [];
1461
+ activeCandidateIndex = 0;
1462
+ leafElement = null;
1463
+ previousCursor = "";
1464
+
1465
+ boundMouseMove ;
1466
+ boundClickCapture ;
1467
+ boundPointerCapture ;
1468
+ boundWheelCapture ;
1469
+ boundKeyDown ;
1470
+ boundScrollOrResize ;
1471
+
1472
+ constructor(callbacks ) {
1473
+ this.callbacks = callbacks;
1474
+ this.boundMouseMove = this.handleMouseMove.bind(this);
1475
+ this.boundClickCapture = this.handleClickCapture.bind(this);
1476
+ this.boundPointerCapture = this.handlePointerSuppression.bind(this);
1477
+ this.boundWheelCapture = this.handleWheelCapture.bind(this);
1478
+ this.boundKeyDown = this.handleKeyDown.bind(this);
1479
+ this.boundScrollOrResize = this.updateOverlayPosition.bind(this);
1480
+ }
1481
+
1482
+ isActive() {
1483
+ return this.active;
1484
+ }
1485
+
1486
+ isAnchored() {
1487
+ return this.anchored;
1488
+ }
1489
+
1490
+ getActiveCandidate() {
1491
+ return this.candidateStack[this.activeCandidateIndex] || null;
1492
+ }
1493
+
1494
+ getCandidateStack() {
1495
+ return [...this.candidateStack];
1496
+ }
1497
+
1498
+ start() {
1499
+ if (this.active || typeof document === "undefined" || typeof window === "undefined") {
1500
+ return;
1501
+ }
1502
+ this.active = true;
1503
+ this.anchored = false;
1504
+ this.candidateStack = [];
1505
+ this.activeCandidateIndex = 0;
1506
+ this.leafElement = null;
1507
+
1508
+ this.hideExternalPreview();
1509
+
1510
+ this.previousCursor = document.body ? document.body.style.cursor : "";
1511
+ if (document.body) {
1512
+ document.body.style.cursor = "crosshair";
1513
+ }
1514
+
1515
+ this.mountSelectionUi();
1516
+ this.notifyStateChange();
1517
+
1518
+ window.addEventListener("mousemove", this.boundMouseMove, { capture: true, passive: true });
1519
+ window.addEventListener("pointerdown", this.boundPointerCapture, { capture: true });
1520
+ window.addEventListener("mousedown", this.boundPointerCapture, { capture: true });
1521
+ window.addEventListener("mouseup", this.boundPointerCapture, { capture: true });
1522
+ window.addEventListener("click", this.boundClickCapture, { capture: true });
1523
+ window.addEventListener("wheel", this.boundWheelCapture, { capture: true, passive: false });
1524
+ window.addEventListener("keydown", this.boundKeyDown, { capture: true });
1525
+ window.addEventListener("scroll", this.boundScrollOrResize, { capture: true, passive: true });
1526
+ window.addEventListener("resize", this.boundScrollOrResize, { passive: true });
1527
+ }
1528
+
1529
+ stop() {
1530
+ if (!this.active) return;
1531
+ this.active = false;
1532
+ this.anchored = false;
1533
+ this.candidateStack = [];
1534
+ this.activeCandidateIndex = 0;
1535
+ this.leafElement = null;
1536
+
1537
+ if (typeof document !== "undefined" && document.body) {
1538
+ document.body.style.cursor = this.previousCursor;
1539
+ }
1540
+
1541
+ if (typeof window !== "undefined") {
1542
+ window.removeEventListener("mousemove", this.boundMouseMove, { capture: true });
1543
+ window.removeEventListener("pointerdown", this.boundPointerCapture, { capture: true });
1544
+ window.removeEventListener("mousedown", this.boundPointerCapture, { capture: true });
1545
+ window.removeEventListener("mouseup", this.boundPointerCapture, { capture: true });
1546
+ window.removeEventListener("click", this.boundClickCapture, { capture: true });
1547
+ window.removeEventListener("wheel", this.boundWheelCapture, { capture: true });
1548
+ window.removeEventListener("keydown", this.boundKeyDown, { capture: true });
1549
+ window.removeEventListener("scroll", this.boundScrollOrResize, { capture: true });
1550
+ window.removeEventListener("resize", this.boundScrollOrResize);
1551
+ }
1552
+
1553
+ this.unmountSelectionUi();
1554
+ this.notifyStateChange();
1555
+ this.callbacks.onExit();
1556
+ }
1557
+
1558
+ /**
1559
+ * Allows the widget or breadcrumb bar to select or preview a specific DOM element directly.
1560
+ */
1561
+ focusCandidateElement(el ) {
1562
+ if (!this.active) return;
1563
+ const excluded = this.callbacks.getExcludedSelectors();
1564
+ if (!isSelectableElement(el, excluded)) return;
1565
+
1566
+ const existingIdx = this.candidateStack.findIndex((c) => c.element === el);
1567
+ if (existingIdx !== -1) {
1568
+ this.activeCandidateIndex = existingIdx;
1569
+ } else {
1570
+ const info = describeElementCandidate(el, "ancestor", this.candidateStack.length, excluded);
1571
+ this.candidateStack.push(info);
1572
+ this.activeCandidateIndex = this.candidateStack.length - 1;
1573
+ }
1574
+
1575
+ this.updateOverlayPosition();
1576
+ if (this.anchored) {
1577
+ this.renderLayerPopover();
1578
+ }
1579
+ this.notifyStateChange();
1580
+ }
1581
+
1582
+ selectElementDirectly(el ) {
1583
+ const excluded = this.callbacks.getExcludedSelectors();
1584
+ const snapshot = captureElementSnapshot(el, excluded);
1585
+ if (!snapshot) return false;
1586
+
1587
+ this.callbacks.onSelect(snapshot, el);
1588
+ this.closeLayerPopover();
1589
+
1590
+ if (!this.callbacks.canSelectMore()) {
1591
+ this.stop();
1592
+ } else {
1593
+ this.notifyStateChange();
1594
+ }
1595
+ return true;
1596
+ }
1597
+
1598
+ showExternalPreview(el ) {
1599
+ if (typeof document === "undefined" || !document.body) return;
1600
+ if (!el || this.active) {
1601
+ this.hideExternalPreview();
1602
+ return;
1603
+ }
1604
+
1605
+ const rect = el.getBoundingClientRect();
1606
+ if (rect.width <= 0 || rect.height <= 0) {
1607
+ this.hideExternalPreview();
1608
+ return;
1609
+ }
1610
+
1611
+ if (!this.previewOverlayEl) {
1612
+ const preview = document.createElement("div");
1613
+ preview.setAttribute("data-context-drop-ui", "chip-preview");
1614
+ preview.style.cssText = [
1615
+ "position: fixed",
1616
+ "pointer-events: none",
1617
+ "z-index: 2147483644",
1618
+ "border: 2px solid #c2410c",
1619
+ "background: rgba(234, 88, 12, 0.10)",
1620
+ "border-radius: 6px",
1621
+ "transition: all 0.12s cubic-bezier(0.22, 1, 0.36, 1)"
1622
+ ].join(";");
1623
+ document.body.appendChild(preview);
1624
+ this.previewOverlayEl = preview;
1625
+ }
1626
+
1627
+ this.previewOverlayEl.style.display = "block";
1628
+ this.previewOverlayEl.style.left = `${rect.left}px`;
1629
+ this.previewOverlayEl.style.top = `${rect.top}px`;
1630
+ this.previewOverlayEl.style.width = `${rect.width}px`;
1631
+ this.previewOverlayEl.style.height = `${rect.height}px`;
1632
+ }
1633
+
1634
+ hideExternalPreview() {
1635
+ if (this.previewOverlayEl && this.previewOverlayEl.parentNode) {
1636
+ this.previewOverlayEl.parentNode.removeChild(this.previewOverlayEl);
1637
+ }
1638
+ this.previewOverlayEl = null;
1639
+ }
1640
+
1641
+ cycleDepth(delta ) {
1642
+ if (!this.active || this.candidateStack.length <= 1) return;
1643
+ const next = Math.max(
1644
+ 0,
1645
+ Math.min(this.candidateStack.length - 1, this.activeCandidateIndex + delta)
1646
+ );
1647
+ if (next !== this.activeCandidateIndex) {
1648
+ this.activeCandidateIndex = next;
1649
+ this.updateOverlayPosition();
1650
+ if (this.anchored) {
1651
+ this.renderLayerPopover();
1652
+ }
1653
+ this.notifyStateChange();
1654
+ }
1655
+ }
1656
+
1657
+ notifyStateChange() {
1658
+ if (!this.callbacks.onStateChange) return;
1659
+ const activeCandidate = this.getActiveCandidate();
1660
+ const ancestors = activeCandidate
1661
+ ? resolveAncestorChain(activeCandidate.element, this.callbacks.getExcludedSelectors())
1662
+ : [];
1663
+ this.callbacks.onStateChange({
1664
+ active: this.active,
1665
+ anchored: this.anchored,
1666
+ activeCandidate,
1667
+ candidateStack: [...this.candidateStack],
1668
+ ancestors
1669
+ });
1670
+ }
1671
+
1672
+ mountSelectionUi() {
1673
+ if (typeof document === "undefined" || !document.body) return;
1674
+
1675
+ // Secondary ghost outline showing the innermost leaf element when cycling up to parent/overlapping layers
1676
+ const leafOutline = document.createElement("div");
1677
+ leafOutline.setAttribute("data-context-drop-ui", "leaf-outline");
1678
+ leafOutline.style.cssText = [
1679
+ "position: fixed",
1680
+ "pointer-events: none",
1681
+ "z-index: 2147483644",
1682
+ "border: 1px dashed rgba(29, 78, 216, 0.55)",
1683
+ "background: rgba(29, 78, 216, 0.03)",
1684
+ "border-radius: 4px",
1685
+ "display: none"
1686
+ ].join(";");
1687
+
1688
+ // Primary active element highlight box
1689
+ const overlay = document.createElement("div");
1690
+ overlay.setAttribute("data-context-drop-ui", "overlay");
1691
+ overlay.style.cssText = [
1692
+ "position: fixed",
1693
+ "pointer-events: none",
1694
+ "z-index: 2147483645",
1695
+ "border: 2px solid #1d4ed8",
1696
+ "background: rgba(29, 78, 216, 0.10)",
1697
+ "border-radius: 6px",
1698
+ "display: none",
1699
+ "transition: left 0.06s cubic-bezier(0.22, 1, 0.36, 1), top 0.06s cubic-bezier(0.22, 1, 0.36, 1), width 0.06s cubic-bezier(0.22, 1, 0.36, 1), height 0.06s cubic-bezier(0.22, 1, 0.36, 1)"
1700
+ ].join(";");
1701
+
1702
+ // Floating HUD badge on the highlight box
1703
+ const label = document.createElement("div");
1704
+ label.setAttribute("data-context-drop-ui", "label");
1705
+ label.style.cssText = [
1706
+ "position: absolute",
1707
+ "top: -30px",
1708
+ "left: -2px",
1709
+ "background: #111318",
1710
+ "color: #faf9f6",
1711
+ "font-family: ui-monospace, SFMono-Regular, Menlo, monospace",
1712
+ "font-size: 11px",
1713
+ "font-variant-numeric: tabular-nums",
1714
+ "line-height: 1.3",
1715
+ "padding: 4px 8px",
1716
+ "border-radius: 6px",
1717
+ "white-space: nowrap",
1718
+ "pointer-events: none",
1719
+ "display: inline-flex",
1720
+ "align-items: center",
1721
+ "gap: 6px",
1722
+ "box-shadow: 0 8px 18px -4px rgba(17, 19, 24, 0.28)"
1723
+ ].join(";");
1724
+ overlay.appendChild(label);
1725
+
1726
+ // Floating Pin marker that follows the pointer or anchors at the clicked coordinate
1727
+ const pinMarker = document.createElement("div");
1728
+ pinMarker.setAttribute("data-context-drop-ui", "pin-marker");
1729
+ pinMarker.style.cssText = [
1730
+ "position: fixed",
1731
+ "pointer-events: none",
1732
+ "z-index: 2147483646",
1733
+ "width: 28px",
1734
+ "height: 28px",
1735
+ "margin-left: -6px",
1736
+ "margin-top: -24px",
1737
+ "border-radius: 9999px 9999px 9999px 3px",
1738
+ "background: #c2410c",
1739
+ "color: #faf9f6",
1740
+ "border: 1.5px solid #faf9f6",
1741
+ "display: none",
1742
+ "align-items: center",
1743
+ "justify-content: center",
1744
+ "box-shadow: 0 8px 16px -3px rgba(17, 19, 24, 0.32)",
1745
+ "transition: transform 0.12s cubic-bezier(0.22, 1, 0.36, 1)"
1746
+ ].join(";");
1747
+ pinMarker.innerHTML = ICON_PIN;
1748
+
1749
+ // Layer & Hierarchy Disambiguation Popover (anchors when clicking overlapping/cascading elements)
1750
+ const popover = document.createElement("div");
1751
+ popover.setAttribute("data-context-drop-ui", "layer-popover");
1752
+ popover.style.cssText = [
1753
+ "position: fixed",
1754
+ "z-index: 2147483647",
1755
+ "width: 348px",
1756
+ "max-width: calc(100vw - 24px)",
1757
+ "background: #faf9f6",
1758
+ "color: #111318",
1759
+ "border: 1px solid #d6d3cd",
1760
+ "border-radius: 12px",
1761
+ "box-shadow: 0 22px 44px -12px rgba(17, 19, 24, 0.26), 0 6px 14px -4px rgba(17, 19, 24, 0.10)",
1762
+ "font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
1763
+ "font-size: 12px",
1764
+ "display: none",
1765
+ "overflow: hidden",
1766
+ "pointer-events: auto"
1767
+ ].join(";");
1768
+
1769
+ document.body.appendChild(leafOutline);
1770
+ document.body.appendChild(overlay);
1771
+ document.body.appendChild(pinMarker);
1772
+ document.body.appendChild(popover);
1773
+
1774
+ this.leafOutlineEl = leafOutline;
1775
+ this.overlayEl = overlay;
1776
+ this.labelEl = label;
1777
+ this.pinMarkerEl = pinMarker;
1778
+ this.popoverEl = popover;
1779
+ }
1780
+
1781
+ unmountSelectionUi() {
1782
+ for (const el of [this.leafOutlineEl, this.overlayEl, this.pinMarkerEl, this.popoverEl]) {
1783
+ if (el && el.parentNode) {
1784
+ el.parentNode.removeChild(el);
1785
+ }
1786
+ }
1787
+ this.leafOutlineEl = null;
1788
+ this.overlayEl = null;
1789
+ this.labelEl = null;
1790
+ this.pinMarkerEl = null;
1791
+ this.popoverEl = null;
1792
+ }
1793
+
1794
+ isInternalUiEvent(event ) {
1795
+ const path = typeof event.composedPath === "function" ? event.composedPath() : [];
1796
+ for (const node of path) {
1797
+ if (node instanceof Element) {
1798
+ if (
1799
+ node.tagName.toLowerCase() === "context-drop-widget" ||
1800
+ node.hasAttribute("data-context-drop-ui") ||
1801
+ (typeof node.closest === "function" &&
1802
+ (node.closest("context-drop-widget") || node.closest("[data-context-drop-ui]")))
1803
+ ) {
1804
+ return true;
1805
+ }
1806
+ }
1807
+ }
1808
+ const rawTarget = event.target instanceof Element ? event.target : null;
1809
+ if (
1810
+ rawTarget &&
1811
+ typeof rawTarget.closest === "function" &&
1812
+ (rawTarget.closest("context-drop-widget") || rawTarget.closest("[data-context-drop-ui]"))
1813
+ ) {
1814
+ return true;
1815
+ }
1816
+ return false;
1817
+ }
1818
+
1819
+ collectPointElements(clientX , clientY , event ) {
1820
+ const results = [];
1821
+ if (typeof document !== "undefined" && typeof document.elementsFromPoint === "function") {
1822
+ try {
1823
+ const fromPoint = document.elementsFromPoint(clientX, clientY);
1824
+ for (const el of fromPoint) {
1825
+ if (el instanceof Element) {
1826
+ results.push(el);
1827
+ }
1828
+ }
1829
+ } catch {
1830
+ // Fallback to composedPath below
1831
+ }
1832
+ }
1833
+ if (event && typeof event.composedPath === "function") {
1834
+ for (const node of event.composedPath()) {
1835
+ if (node instanceof Element && !results.includes(node)) {
1836
+ results.push(node);
1837
+ }
1838
+ }
1839
+ }
1840
+ if (event && event.target instanceof Element && !results.includes(event.target)) {
1841
+ results.unshift(event.target);
1842
+ }
1843
+ return results;
1844
+ }
1845
+
1846
+ updateCandidatesFromPoint(clientX , clientY , event ) {
1847
+ const excluded = this.callbacks.getExcludedSelectors();
1848
+ const pointElements = this.collectPointElements(clientX, clientY, event);
1849
+ const primary = pointElements.find((el) => isSelectableElement(el, excluded)) || null;
1850
+
1851
+ if (!primary) {
1852
+ this.leafElement = null;
1853
+ this.candidateStack = [];
1854
+ this.activeCandidateIndex = 0;
1855
+ return;
1856
+ }
1857
+
1858
+ // Preserve depth selection if user is hovering inside the exact same primary leaf element
1859
+ if (this.leafElement === primary && this.candidateStack.length > 0) {
1860
+ return;
1861
+ }
1862
+
1863
+ this.leafElement = primary;
1864
+ this.candidateStack = resolveCandidateStack(primary, pointElements, excluded, 10);
1865
+ this.activeCandidateIndex = 0;
1866
+ }
1867
+
1868
+ handleMouseMove(event ) {
1869
+ if (!this.active) return;
1870
+
1871
+ this.lastPointerX = event.clientX;
1872
+ this.lastPointerY = event.clientY;
1873
+
1874
+ if (this.anchored) {
1875
+ return;
1876
+ }
1877
+
1878
+ if (this.isInternalUiEvent(event)) {
1879
+ if (this.overlayEl) this.overlayEl.style.display = "none";
1880
+ if (this.leafOutlineEl) this.leafOutlineEl.style.display = "none";
1881
+ if (this.pinMarkerEl) this.pinMarkerEl.style.display = "none";
1882
+ return;
1883
+ }
1884
+
1885
+ if (this.pinMarkerEl) {
1886
+ this.pinMarkerEl.style.display = "inline-flex";
1887
+ this.pinMarkerEl.style.left = `${event.clientX}px`;
1888
+ this.pinMarkerEl.style.top = `${event.clientY}px`;
1889
+ }
1890
+
1891
+ this.updateCandidatesFromPoint(event.clientX, event.clientY, event);
1892
+ this.updateOverlayPosition();
1893
+ this.notifyStateChange();
1894
+ }
1895
+
1896
+ updateOverlayPosition() {
1897
+ if (!this.overlayEl || !this.labelEl) return;
1898
+
1899
+ const currentCandidate = this.getActiveCandidate();
1900
+ if (!currentCandidate) {
1901
+ this.overlayEl.style.display = "none";
1902
+ if (this.leafOutlineEl) this.leafOutlineEl.style.display = "none";
1903
+ return;
1904
+ }
1905
+
1906
+ const rect = currentCandidate.element.getBoundingClientRect();
1907
+ if (rect.width <= 0 || rect.height <= 0) {
1908
+ this.overlayEl.style.display = "none";
1909
+ return;
1910
+ }
1911
+
1912
+ this.overlayEl.style.display = "block";
1913
+ this.overlayEl.style.left = `${rect.left}px`;
1914
+ this.overlayEl.style.top = `${rect.top}px`;
1915
+ this.overlayEl.style.width = `${rect.width}px`;
1916
+ this.overlayEl.style.height = `${rect.height}px`;
1917
+
1918
+ // Position the HUD pill inside the top of the box if the element is near the top of the viewport
1919
+ if (rect.top < 36) {
1920
+ this.labelEl.style.top = "6px";
1921
+ this.labelEl.style.left = "6px";
1922
+ } else {
1923
+ this.labelEl.style.top = "-30px";
1924
+ this.labelEl.style.left = "-2px";
1925
+ }
1926
+
1927
+ const total = this.candidateStack.length;
1928
+ const pos = this.activeCandidateIndex + 1;
1929
+ const dims = `${Math.round(rect.width)}×${Math.round(rect.height)}`;
1930
+ const depthBadge = total > 1 ? ` · ${pos}/${total} layers (↕ scroll)` : "";
1931
+ this.labelEl.textContent = `${currentCandidate.tagName}${currentCandidate.selector.startsWith(currentCandidate.tagName) ? currentCandidate.selector.slice(currentCandidate.tagName.length) : ` ${currentCandidate.selector}`} · ${dims}${depthBadge}`;
1932
+
1933
+ // Show ghost leaf outline when inspecting an ancestor or overlapping layer
1934
+ if (this.leafOutlineEl) {
1935
+ if (this.activeCandidateIndex > 0 && this.leafElement && this.leafElement !== currentCandidate.element) {
1936
+ const leafRect = this.leafElement.getBoundingClientRect();
1937
+ this.leafOutlineEl.style.display = "block";
1938
+ this.leafOutlineEl.style.left = `${leafRect.left}px`;
1939
+ this.leafOutlineEl.style.top = `${leafRect.top}px`;
1940
+ this.leafOutlineEl.style.width = `${leafRect.width}px`;
1941
+ this.leafOutlineEl.style.height = `${leafRect.height}px`;
1942
+ } else {
1943
+ this.leafOutlineEl.style.display = "none";
1944
+ }
1945
+ }
1946
+ }
1947
+
1948
+ handleWheelCapture(event ) {
1949
+ if (!this.active) return;
1950
+
1951
+ // Allow natural scrolling inside the layer popover or widget itself
1952
+ if (this.isInternalUiEvent(event)) {
1953
+ return;
1954
+ }
1955
+
1956
+ if (this.candidateStack.length <= 1) {
1957
+ return;
1958
+ }
1959
+
1960
+ event.preventDefault();
1961
+ event.stopPropagation();
1962
+
1963
+ this.wheelAccumulator += event.deltaY;
1964
+ if (Math.abs(this.wheelAccumulator) >= 24) {
1965
+ const step = this.wheelAccumulator > 0 ? 1 : -1;
1966
+ this.wheelAccumulator = 0;
1967
+ this.cycleDepth(step);
1968
+ }
1969
+ }
1970
+
1971
+ handlePointerSuppression(event ) {
1972
+ if (!this.active) return;
1973
+ if (this.isInternalUiEvent(event)) {
1974
+ return;
1975
+ }
1976
+
1977
+ // Intercept host page pointer events in capture phase so links/buttons do not fire
1978
+ event.preventDefault();
1979
+ event.stopPropagation();
1980
+ if (typeof event.stopImmediatePropagation === "function") {
1981
+ event.stopImmediatePropagation();
1982
+ }
1983
+ }
1984
+
1985
+ handleClickCapture(event ) {
1986
+ if (!this.active) return;
1987
+ if (this.isInternalUiEvent(event)) {
1988
+ return;
1989
+ }
1990
+
1991
+ event.preventDefault();
1992
+ event.stopPropagation();
1993
+ if (typeof event.stopImmediatePropagation === "function") {
1994
+ event.stopImmediatePropagation();
1995
+ }
1996
+
1997
+ // If popover was already anchored and user clicked elsewhere on the page, re-evaluate at the new click point
1998
+ this.anchorX = event.clientX;
1999
+ this.anchorY = event.clientY;
2000
+ this.updateCandidatesFromPoint(event.clientX, event.clientY, event);
2001
+
2002
+ const activeCandidate = this.getActiveCandidate();
2003
+ if (!activeCandidate) return;
2004
+
2005
+ // Fast-path: Shift+Click or single-layer target pins immediately
2006
+ if (event.shiftKey || this.candidateStack.length === 1) {
2007
+ this.selectElementDirectly(activeCandidate.element);
2008
+ return;
2009
+ }
2010
+
2011
+ // Otherwise anchor the floating pin and open the Layer & Hierarchy Disambiguation Popover
2012
+ this.anchored = true;
2013
+ if (this.pinMarkerEl) {
2014
+ this.pinMarkerEl.style.display = "inline-flex";
2015
+ this.pinMarkerEl.style.left = `${this.anchorX}px`;
2016
+ this.pinMarkerEl.style.top = `${this.anchorY}px`;
2017
+ this.pinMarkerEl.style.transform = "scale(1.08)";
2018
+ }
2019
+ this.updateOverlayPosition();
2020
+ this.renderLayerPopover();
2021
+ this.notifyStateChange();
2022
+ }
2023
+
2024
+ closeLayerPopover() {
2025
+ this.anchored = false;
2026
+ if (this.popoverEl) {
2027
+ this.popoverEl.style.display = "none";
2028
+ this.popoverEl.innerHTML = "";
2029
+ }
2030
+ if (this.pinMarkerEl) {
2031
+ this.pinMarkerEl.style.transform = "scale(1)";
2032
+ this.pinMarkerEl.style.left = `${this.lastPointerX}px`;
2033
+ this.pinMarkerEl.style.top = `${this.lastPointerY}px`;
2034
+ }
2035
+ this.notifyStateChange();
2036
+ }
2037
+
2038
+ renderLayerPopover() {
2039
+ if (!this.popoverEl || typeof window === "undefined") return;
2040
+ const activeCandidate = this.getActiveCandidate();
2041
+ if (!activeCandidate) return;
2042
+
2043
+ this.popoverEl.innerHTML = "";
2044
+ this.popoverEl.style.display = "block";
2045
+
2046
+ // Header: title + Pin Current CTA + close popover
2047
+ const header = document.createElement("div");
2048
+ header.style.cssText = [
2049
+ "display: flex",
2050
+ "align-items: center",
2051
+ "justify-content: space-between",
2052
+ "gap: 8px",
2053
+ "padding: 10px 12px",
2054
+ "background: #f1efea",
2055
+ "border-bottom: 1px solid #d6d3cd"
2056
+ ].join(";");
2057
+
2058
+ const titleGroup = document.createElement("div");
2059
+ titleGroup.style.cssText = "display: flex; align-items: center; gap: 6px; font-weight: 600; font-size: 12px; color: #111318;";
2060
+ const layersIconSpan = document.createElement("span");
2061
+ layersIconSpan.style.cssText = "display: inline-flex; color: #c2410c;";
2062
+ layersIconSpan.innerHTML = ICON_LAYERS;
2063
+ const titleText = document.createElement("span");
2064
+ titleText.textContent = `Choose Element Layer (${this.candidateStack.length})`;
2065
+ titleGroup.appendChild(layersIconSpan);
2066
+ titleGroup.appendChild(titleText);
2067
+
2068
+ const headerActions = document.createElement("div");
2069
+ headerActions.style.cssText = "display: flex; align-items: center; gap: 4px;";
2070
+
2071
+ const stepUpBtn = document.createElement("button");
2072
+ stepUpBtn.type = "button";
2073
+ stepUpBtn.title = "Step to parent/outer layer (Up arrow)";
2074
+ stepUpBtn.setAttribute("aria-label", "Step to outer layer");
2075
+ stepUpBtn.style.cssText = [
2076
+ "display: inline-flex",
2077
+ "align-items: center",
2078
+ "justify-content: center",
2079
+ "width: 24px",
2080
+ "height: 24px",
2081
+ "border-radius: 6px",
2082
+ "border: 1px solid #d6d3cd",
2083
+ "background: #faf9f6",
2084
+ "color: #111318",
2085
+ "cursor: pointer"
2086
+ ].join(";");
2087
+ stepUpBtn.innerHTML = ICON_CHEVRON_UP;
2088
+ stepUpBtn.disabled = this.activeCandidateIndex >= this.candidateStack.length - 1;
2089
+ stepUpBtn.addEventListener("click", (ev) => {
2090
+ ev.stopPropagation();
2091
+ this.cycleDepth(1);
2092
+ });
2093
+
2094
+ const stepDownBtn = document.createElement("button");
2095
+ stepDownBtn.type = "button";
2096
+ stepDownBtn.title = "Step to inner/child layer (Down arrow)";
2097
+ stepDownBtn.setAttribute("aria-label", "Step to inner layer");
2098
+ stepDownBtn.style.cssText = stepUpBtn.style.cssText;
2099
+ stepDownBtn.innerHTML = ICON_CHEVRON_DOWN;
2100
+ stepDownBtn.disabled = this.activeCandidateIndex <= 0;
2101
+ stepDownBtn.addEventListener("click", (ev) => {
2102
+ ev.stopPropagation();
2103
+ this.cycleDepth(-1);
2104
+ });
2105
+
2106
+ const closeBtn = document.createElement("button");
2107
+ closeBtn.type = "button";
2108
+ closeBtn.title = "Resume hover selection (Esc)";
2109
+ closeBtn.setAttribute("aria-label", "Close layer picker");
2110
+ closeBtn.style.cssText = [
2111
+ "display: inline-flex",
2112
+ "align-items: center",
2113
+ "justify-content: center",
2114
+ "width: 24px",
2115
+ "height: 24px",
2116
+ "border-radius: 6px",
2117
+ "border: 0",
2118
+ "background: transparent",
2119
+ "color: #4b5262",
2120
+ "cursor: pointer"
2121
+ ].join(";");
2122
+ closeBtn.innerHTML = ICON_CLOSE;
2123
+ closeBtn.addEventListener("click", (ev) => {
2124
+ ev.stopPropagation();
2125
+ this.closeLayerPopover();
2126
+ });
2127
+
2128
+ headerActions.appendChild(stepDownBtn);
2129
+ headerActions.appendChild(stepUpBtn);
2130
+ headerActions.appendChild(closeBtn);
2131
+ header.appendChild(titleGroup);
2132
+ header.appendChild(headerActions);
2133
+ this.popoverEl.appendChild(header);
2134
+
2135
+ // Interactive DOM Breadcrumb Bar
2136
+ const ancestors = resolveAncestorChain(
2137
+ activeCandidate.element,
2138
+ this.callbacks.getExcludedSelectors(),
2139
+ 6
2140
+ );
2141
+ if (ancestors.length > 0) {
2142
+ const breadcrumbBar = document.createElement("div");
2143
+ breadcrumbBar.setAttribute("data-context-drop-ui", "breadcrumb-bar");
2144
+ breadcrumbBar.style.cssText = [
2145
+ "display: flex",
2146
+ "align-items: center",
2147
+ "flex-wrap: wrap",
2148
+ "gap: 3px",
2149
+ "padding: 7px 12px",
2150
+ "background: #f5f3ee",
2151
+ "border-bottom: 1px solid #e4e1da",
2152
+ "font-family: ui-monospace, SFMono-Regular, Menlo, monospace",
2153
+ "font-size: 11px"
2154
+ ].join(";");
2155
+
2156
+ ancestors.forEach((ancEl, idx) => {
2157
+ if (idx > 0) {
2158
+ const sep = document.createElement("span");
2159
+ sep.style.cssText = "display: inline-flex; color: #687082;";
2160
+ sep.innerHTML = ICON_CHEVRON_RIGHT;
2161
+ breadcrumbBar.appendChild(sep);
2162
+ }
2163
+
2164
+ const isCurrent = ancEl === activeCandidate.element;
2165
+ const crumbBtn = document.createElement("button");
2166
+ crumbBtn.type = "button";
2167
+ crumbBtn.textContent = computeSelector(ancEl);
2168
+ crumbBtn.style.cssText = [
2169
+ "appearance: none",
2170
+ "border: 1px solid " + (isCurrent ? "#1d4ed8" : "transparent"),
2171
+ "background: " + (isCurrent ? "#dbeafe" : "transparent"),
2172
+ "color: " + (isCurrent ? "#1e3a8a" : "#4b5262"),
2173
+ "font-family: inherit",
2174
+ "font-size: 11px",
2175
+ "font-weight: " + (isCurrent ? "600" : "500"),
2176
+ "padding: 2px 5px",
2177
+ "border-radius: 4px",
2178
+ "cursor: pointer",
2179
+ "max-width: 130px",
2180
+ "overflow: hidden",
2181
+ "text-overflow: ellipsis",
2182
+ "white-space: nowrap"
2183
+ ].join(";");
2184
+
2185
+ crumbBtn.addEventListener("mouseenter", () => {
2186
+ this.focusCandidateElement(ancEl);
2187
+ });
2188
+ crumbBtn.addEventListener("click", (ev) => {
2189
+ ev.stopPropagation();
2190
+ this.selectElementDirectly(ancEl);
2191
+ });
2192
+
2193
+ breadcrumbBar.appendChild(crumbBtn);
2194
+ });
2195
+
2196
+ this.popoverEl.appendChild(breadcrumbBar);
2197
+ }
2198
+
2199
+ // Layer Stack List (Target, Overlapping siblings, Direct children, Ancestor containers)
2200
+ const list = document.createElement("div");
2201
+ list.style.cssText = [
2202
+ "max-height: 230px",
2203
+ "overflow-y: auto",
2204
+ "padding: 6px",
2205
+ "display: flex",
2206
+ "flex-direction: column",
2207
+ "gap: 3px"
2208
+ ].join(";");
2209
+
2210
+ const relationLabels = {
2211
+ target: "Topmost",
2212
+ overlap: "Overlap",
2213
+ child: "Child",
2214
+ ancestor: "Parent"
2215
+ };
2216
+
2217
+ this.candidateStack.forEach((candidate, idx) => {
2218
+ const isSelected = idx === this.activeCandidateIndex;
2219
+ const row = document.createElement("button");
2220
+ row.type = "button";
2221
+ row.setAttribute("data-context-drop-layer-index", String(idx));
2222
+ row.style.cssText = [
2223
+ "appearance: none",
2224
+ "width: 100%",
2225
+ "text-align: left",
2226
+ "display: flex",
2227
+ "flex-direction: column",
2228
+ "gap: 3px",
2229
+ "padding: 7px 9px",
2230
+ "border-radius: 8px",
2231
+ "border: 1px solid " + (isSelected ? "#1d4ed8" : "transparent"),
2232
+ "background: " + (isSelected ? "#eff6ff" : "transparent"),
2233
+ "color: #111318",
2234
+ "cursor: pointer",
2235
+ "transition: background-color 0.1s ease"
2236
+ ].join(";");
2237
+
2238
+ const topLine = document.createElement("div");
2239
+ topLine.style.cssText = "display: flex; align-items: center; justify-content: space-between; gap: 6px; width: 100%;";
2240
+
2241
+ const leftGroup = document.createElement("div");
2242
+ leftGroup.style.cssText = "display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1;";
2243
+
2244
+ const relBadge = document.createElement("span");
2245
+ relBadge.textContent = relationLabels[candidate.relation] || "Layer";
2246
+ relBadge.style.cssText = [
2247
+ "font-size: 10px",
2248
+ "font-weight: 600",
2249
+ "padding: 1px 5px",
2250
+ "border-radius: 4px",
2251
+ "background: " + (candidate.relation === "target" ? "#ffedd5" : candidate.relation === "overlap" ? "#fce7f3" : "#e2e8f0"),
2252
+ "color: " + (candidate.relation === "target" ? "#9a3412" : candidate.relation === "overlap" ? "#9d174d" : "#334155"),
2253
+ "flex-shrink: 0"
2254
+ ].join(";");
2255
+
2256
+ const selectorSpan = document.createElement("span");
2257
+ selectorSpan.textContent = `${candidate.tagName}${candidate.selector.startsWith(candidate.tagName) ? candidate.selector.slice(candidate.tagName.length) : candidate.selector}`;
2258
+ selectorSpan.style.cssText = [
2259
+ "font-family: ui-monospace, SFMono-Regular, Menlo, monospace",
2260
+ "font-size: 11.5px",
2261
+ "font-weight: 600",
2262
+ "color: #111318",
2263
+ "overflow: hidden",
2264
+ "text-overflow: ellipsis",
2265
+ "white-space: nowrap"
2266
+ ].join(";");
2267
+
2268
+ leftGroup.appendChild(relBadge);
2269
+ leftGroup.appendChild(selectorSpan);
2270
+
2271
+ const dimsSpan = document.createElement("span");
2272
+ dimsSpan.textContent = `${Math.round(candidate.rect.width)}×${Math.round(candidate.rect.height)}`;
2273
+ dimsSpan.style.cssText = [
2274
+ "font-family: ui-monospace, SFMono-Regular, Menlo, monospace",
2275
+ "font-size: 10.5px",
2276
+ "font-variant-numeric: tabular-nums",
2277
+ "color: #4b5262",
2278
+ "flex-shrink: 0"
2279
+ ].join(";");
2280
+
2281
+ topLine.appendChild(leftGroup);
2282
+ topLine.appendChild(dimsSpan);
2283
+ row.appendChild(topLine);
2284
+
2285
+ if (candidate.textPreview) {
2286
+ const previewLine = document.createElement("div");
2287
+ previewLine.textContent = `"${candidate.textPreview}"`;
2288
+ previewLine.style.cssText = [
2289
+ "font-size: 11px",
2290
+ "color: #4b5262",
2291
+ "overflow: hidden",
2292
+ "text-overflow: ellipsis",
2293
+ "white-space: nowrap",
2294
+ "padding-left: 2px"
2295
+ ].join(";");
2296
+ row.appendChild(previewLine);
2297
+ }
2298
+
2299
+ row.addEventListener("mouseenter", () => {
2300
+ if (this.activeCandidateIndex !== idx) {
2301
+ this.activeCandidateIndex = idx;
2302
+ this.updateOverlayPosition();
2303
+ this.notifyStateChange();
2304
+ }
2305
+ });
2306
+
2307
+ row.addEventListener("click", (ev) => {
2308
+ ev.stopPropagation();
2309
+ this.selectElementDirectly(candidate.element);
2310
+ });
2311
+
2312
+ list.appendChild(row);
2313
+ });
2314
+
2315
+ this.popoverEl.appendChild(list);
2316
+
2317
+ // Footer with primary Pin confirmation action
2318
+ const footer = document.createElement("div");
2319
+ footer.style.cssText = [
2320
+ "display: flex",
2321
+ "align-items: center",
2322
+ "justify-content: space-between",
2323
+ "padding: 8px 12px",
2324
+ "background: #f1efea",
2325
+ "border-top: 1px solid #d6d3cd"
2326
+ ].join(";");
2327
+
2328
+ const hint = document.createElement("span");
2329
+ hint.textContent = "Click any layer or press Enter";
2330
+ hint.style.cssText = "font-size: 11px; color: #4b5262;";
2331
+
2332
+ const pinNowBtn = document.createElement("button");
2333
+ pinNowBtn.type = "button";
2334
+ pinNowBtn.setAttribute("data-context-drop-ui", "confirm-pin-btn");
2335
+ pinNowBtn.style.cssText = [
2336
+ "display: inline-flex",
2337
+ "align-items: center",
2338
+ "gap: 5px",
2339
+ "padding: 5px 10px",
2340
+ "border-radius: 6px",
2341
+ "border: 0",
2342
+ "background: #c2410c",
2343
+ "color: #faf9f6",
2344
+ "font-size: 11.5px",
2345
+ "font-weight: 600",
2346
+ "cursor: pointer"
2347
+ ].join(";");
2348
+ pinNowBtn.innerHTML = `${ICON_CHECK}<span>Pin Selected</span>`;
2349
+ pinNowBtn.addEventListener("click", (ev) => {
2350
+ ev.stopPropagation();
2351
+ const target = this.getActiveCandidate();
2352
+ if (target) {
2353
+ this.selectElementDirectly(target.element);
2354
+ }
2355
+ });
2356
+
2357
+ footer.appendChild(hint);
2358
+ footer.appendChild(pinNowBtn);
2359
+ this.popoverEl.appendChild(footer);
2360
+
2361
+ // Clamp popover inside viewport bounds near anchorX, anchorY
2362
+ const vw = window.innerWidth || 1024;
2363
+ const vh = window.innerHeight || 768;
2364
+ const popWidth = 348;
2365
+ const popHeight = 310;
2366
+ const left = Math.max(12, Math.min(this.anchorX + 14, vw - popWidth - 12));
2367
+ const top =
2368
+ this.anchorY + popHeight + 20 > vh
2369
+ ? Math.max(12, this.anchorY - popHeight - 12)
2370
+ : Math.max(12, this.anchorY + 14);
2371
+
2372
+ this.popoverEl.style.left = `${left}px`;
2373
+ this.popoverEl.style.top = `${top}px`;
2374
+ }
2375
+
2376
+ handleKeyDown(event ) {
2377
+ if (!this.active) return;
2378
+
2379
+ if (event.key === "Escape") {
2380
+ event.preventDefault();
2381
+ event.stopPropagation();
2382
+ if (this.anchored) {
2383
+ this.closeLayerPopover();
2384
+ } else {
2385
+ this.stop();
2386
+ }
2387
+ return;
2388
+ }
2389
+
2390
+ if (event.key === "ArrowUp" || event.key === "[") {
2391
+ event.preventDefault();
2392
+ event.stopPropagation();
2393
+ this.cycleDepth(1);
2394
+ return;
2395
+ }
2396
+
2397
+ if (event.key === "ArrowDown" || event.key === "]") {
2398
+ event.preventDefault();
2399
+ event.stopPropagation();
2400
+ this.cycleDepth(-1);
2401
+ return;
2402
+ }
2403
+
2404
+ if (event.key === "Enter") {
2405
+ const candidate = this.getActiveCandidate();
2406
+ if (candidate) {
2407
+ event.preventDefault();
2408
+ event.stopPropagation();
2409
+ this.selectElementDirectly(candidate.element);
2410
+ }
2411
+ }
2412
+ }
2413
+ }
2414
+
2415
+ // --- Module: widget.ts ---
2416
+ const DEFAULT_CONFIG = {
2417
+ enabled: true,
2418
+ position: "top_left",
2419
+ placeholder: "Describe the issue or drop context from the page...",
2420
+ features: {
2421
+ element_selection: true,
2422
+ screenshot_capture: true,
2423
+ element_capture_padding: 16
2424
+ },
2425
+ limits: {
2426
+ max_attachments: 10,
2427
+ max_element_references: 10,
2428
+ max_attachment_size: 10 * 1024 * 1024,
2429
+ allowed_content_types: ["image/png", "image/jpeg", "image/webp", "image/gif"],
2430
+ max_image_dimensions: [4096, 4096]
2431
+ },
2432
+ privacy: {
2433
+ excluded_selectors: Array.from(MANDATORY_EXCLUDED_SELECTORS)
2434
+ }
2435
+ };
2436
+
2437
+ const BaseHTMLElement =
2438
+ typeof HTMLElement !== "undefined"
2439
+ ? HTMLElement
2440
+ : (class {} );
2441
+ class ContextDropWidgetElement extends BaseHTMLElement {
2442
+ shadow = null;
2443
+ config = structuredClone(DEFAULT_CONFIG);
2444
+ customMetadata = {};
2445
+ apiClient = new ContextDropApiClient("/context_drop");
2446
+ picker = null;
2447
+
2448
+ isOpen = false;
2449
+ isMinimized = false;
2450
+ isSubmitting = false;
2451
+ freePos = null;
2452
+
2453
+ elementReferences = [];
2454
+ elementDomNodes = new Map();
2455
+ attachments = [];
2456
+ sessionHistory = [];
2457
+
2458
+ rootContainer = null;
2459
+ composerEl = null;
2460
+ compactBarEl = null;
2461
+ compactBreadcrumbEl = null;
2462
+ compactPinBtn = null;
2463
+ pinDockEl = null;
2464
+ floatingPinBtn = null;
2465
+ triggerBtn = null;
2466
+ cornerPinBtn = null;
2467
+ chatThreadEl = null;
2468
+ textareaEl = null;
2469
+ trayEl = null;
2470
+ statusEl = null;
2471
+ fileInputEl = null;
2472
+ submitBtn = null;
2473
+
2474
+ connectedCallback() {
2475
+ if (!this.shadow) {
2476
+ this.shadow = this.attachShadow({ mode: "open" });
2477
+ }
2478
+ this.readDataAttributes();
2479
+ this.renderShadowDom();
2480
+
2481
+ this.picker = new ElementPicker({
2482
+ onSelect: (snapshot, el) => {
2483
+ const added = this.addElementReference(snapshot, el);
2484
+ if (added) {
2485
+ this.showStatus(`Pinned ${snapshot.selector}`, "success");
2486
+ }
2487
+ this.syncUiStates();
2488
+ },
2489
+ onExit: () => {
2490
+ this.isMinimized = false;
2491
+ this.openComposer();
2492
+ this.syncUiStates();
2493
+ },
2494
+ canSelectMore: () => this.elementReferences.length < this.config.limits.max_element_references,
2495
+ getExcludedSelectors: () => this.config.privacy.excluded_selectors,
2496
+ onStateChange: (state) => {
2497
+ this.renderCompactBreadcrumbs(state.activeCandidate, state.ancestors);
2498
+ this.syncPinActiveIndicators(state.active);
2499
+ }
2500
+ });
2501
+ }
2502
+
2503
+ disconnectedCallback() {
2504
+ if (this.picker && this.picker.isActive()) {
2505
+ this.picker.stop();
2506
+ }
2507
+ this.picker?.hideExternalPreview();
2508
+ }
2509
+
2510
+ getConfig() {
2511
+ return this.config;
2512
+ }
2513
+
2514
+ getElementReferences() {
2515
+ return [...this.elementReferences];
2516
+ }
2517
+
2518
+ getAttachments() {
2519
+ return [...this.attachments];
2520
+ }
2521
+
2522
+ getSessionHistory() {
2523
+ return [...this.sessionHistory];
2524
+ }
2525
+
2526
+ openComposer() {
2527
+ this.isOpen = true;
2528
+ this.isMinimized = false;
2529
+ this.syncUiStates();
2530
+ if (this.textareaEl) {
2531
+ this.textareaEl.focus();
2532
+ }
2533
+ }
2534
+
2535
+ closeComposer() {
2536
+ this.isOpen = false;
2537
+ this.isMinimized = false;
2538
+ if (this.picker && this.picker.isActive()) {
2539
+ this.picker.stop();
2540
+ }
2541
+ this.picker?.hideExternalPreview();
2542
+ this.syncUiStates();
2543
+ }
2544
+
2545
+ toggleComposer() {
2546
+ if (this.isOpen && !this.isMinimized) {
2547
+ this.closeComposer();
2548
+ } else {
2549
+ this.openComposer();
2550
+ }
2551
+ }
2552
+
2553
+ minimizeToCompactBar() {
2554
+ this.isOpen = true;
2555
+ this.isMinimized = true;
2556
+ this.syncUiStates();
2557
+ }
2558
+
2559
+ expandFromCompactBar() {
2560
+ if (this.picker && this.picker.isActive()) {
2561
+ this.picker.stop();
2562
+ }
2563
+ this.isMinimized = false;
2564
+ this.openComposer();
2565
+ }
2566
+
2567
+ startPinSelection() {
2568
+ if (!this.config.features.element_selection || !this.picker) return;
2569
+ if (this.elementReferences.length >= this.config.limits.max_element_references) {
2570
+ this.openComposer();
2571
+ this.showStatus(
2572
+ `Maximum of ${this.config.limits.max_element_references} pinned elements reached.`,
2573
+ "error"
2574
+ );
2575
+ return;
2576
+ }
2577
+ this.minimizeToCompactBar();
2578
+ this.picker.start();
2579
+ this.syncUiStates();
2580
+ }
2581
+
2582
+ togglePinSelection() {
2583
+ if (this.picker && this.picker.isActive()) {
2584
+ this.picker.stop();
2585
+ } else {
2586
+ this.startPinSelection();
2587
+ }
2588
+ }
2589
+
2590
+ setFreePosition(left , top ) {
2591
+ if (!this.rootContainer) return;
2592
+ const vw = typeof window !== "undefined" ? window.innerWidth || 1024 : 1024;
2593
+ const vh = typeof window !== "undefined" ? window.innerHeight || 768 : 768;
2594
+ const clampedLeft = Math.max(8, Math.min(left, vw - 72));
2595
+ const clampedTop = Math.max(8, Math.min(top, vh - 48));
2596
+
2597
+ this.freePos = { left: clampedLeft, top: clampedTop };
2598
+ this.rootContainer.classList.add("is-free-positioned");
2599
+ this.rootContainer.style.left = `${clampedLeft}px`;
2600
+ this.rootContainer.style.top = `${clampedTop}px`;
2601
+ }
2602
+
2603
+ addElementReference(snapshot , elementNode ) {
2604
+ if (this.elementReferences.length >= this.config.limits.max_element_references) {
2605
+ this.showStatus(
2606
+ `Maximum of ${this.config.limits.max_element_references} DOM elements reached.`,
2607
+ "error"
2608
+ );
2609
+ return false;
2610
+ }
2611
+ this.elementReferences.push(snapshot);
2612
+ if (elementNode) {
2613
+ this.elementDomNodes.set(snapshot.client_ref, elementNode);
2614
+ }
2615
+ this.renderTray();
2616
+ this.syncUiStates();
2617
+ return true;
2618
+ }
2619
+
2620
+ removeElementReference(clientRef ) {
2621
+ this.elementReferences = this.elementReferences.filter((el) => el.client_ref !== clientRef);
2622
+ this.elementDomNodes.delete(clientRef);
2623
+ this.picker?.hideExternalPreview();
2624
+ this.renderTray();
2625
+ this.syncUiStates();
2626
+ }
2627
+
2628
+ removeAttachment(attachmentId ) {
2629
+ this.attachments = this.attachments.filter((att) => att.id !== attachmentId);
2630
+ this.renderTray();
2631
+ this.syncUiStates();
2632
+ }
2633
+
2634
+ readDataAttributes() {
2635
+ const endpoint = this.getAttribute("data-endpoint") || "/context_drop";
2636
+ this.apiClient = new ContextDropApiClient(endpoint);
2637
+
2638
+ const rawConfig = this.getAttribute("data-config");
2639
+ if (rawConfig) {
2640
+ try {
2641
+ const parsed = JSON.parse(rawConfig) ;
2642
+ this.config = {
2643
+ ...this.config,
2644
+ ...parsed,
2645
+ features: { ...this.config.features, ...(parsed.features || {}) },
2646
+ limits: { ...this.config.limits, ...(parsed.limits || {}) },
2647
+ privacy: { ...this.config.privacy, ...(parsed.privacy || {}) }
2648
+ };
2649
+ } catch {
2650
+ // Ignore malformed JSON attribute
2651
+ }
2652
+ }
2653
+
2654
+ const posOverride = this.getAttribute("data-position") ;
2655
+ if (
2656
+ posOverride &&
2657
+ ["bottom_right", "bottom_left", "top_right", "top_left"].includes(posOverride)
2658
+ ) {
2659
+ this.config.position = posOverride;
2660
+ }
2661
+
2662
+ const placeholderOverride = this.getAttribute("data-placeholder");
2663
+ if (placeholderOverride) {
2664
+ this.config.placeholder = placeholderOverride;
2665
+ }
2666
+
2667
+ const rawMetadata = this.getAttribute("data-metadata");
2668
+ if (rawMetadata) {
2669
+ try {
2670
+ this.customMetadata = JSON.parse(rawMetadata) ;
2671
+ } catch {
2672
+ this.customMetadata = {};
2673
+ }
2674
+ }
2675
+ }
2676
+
2677
+ attachDragHandle(handleEl , onClickFallback ) {
2678
+ let startX = 0;
2679
+ let startY = 0;
2680
+ let initialLeft = 0;
2681
+ let initialTop = 0;
2682
+ let dragging = false;
2683
+ let moved = false;
2684
+
2685
+ const onPointerMove = (ev ) => {
2686
+ if (!dragging) return;
2687
+ const dx = ev.clientX - startX;
2688
+ const dy = ev.clientY - startY;
2689
+ if (!moved && Math.hypot(dx, dy) > 5) {
2690
+ moved = true;
2691
+ this.pinDockEl?.classList.add("is-dragging");
2692
+ }
2693
+ if (moved) {
2694
+ this.setFreePosition(initialLeft + dx, initialTop + dy);
2695
+ }
2696
+ };
2697
+
2698
+ const onPointerUp = () => {
2699
+ if (!dragging) return;
2700
+ dragging = false;
2701
+ this.pinDockEl?.classList.remove("is-dragging");
2702
+ window.removeEventListener("pointermove", onPointerMove);
2703
+ window.removeEventListener("pointerup", onPointerUp);
2704
+ if (!moved && onClickFallback) {
2705
+ onClickFallback();
2706
+ }
2707
+ };
2708
+
2709
+ handleEl.addEventListener("pointerdown", (ev ) => {
2710
+ // Only primary mouse button or touch
2711
+ if (ev.button !== 0) return;
2712
+ const targetEl = ev.target instanceof Element ? ev.target : null;
2713
+ if (
2714
+ targetEl &&
2715
+ handleEl !== this.pinDockEl &&
2716
+ targetEl.closest("button")
2717
+ ) {
2718
+ return;
2719
+ }
2720
+
2721
+ if (!this.rootContainer) return;
2722
+ const rect = this.rootContainer.getBoundingClientRect();
2723
+ startX = ev.clientX;
2724
+ startY = ev.clientY;
2725
+ initialLeft = this.freePos ? this.freePos.left : rect.left;
2726
+ initialTop = this.freePos ? this.freePos.top : rect.top;
2727
+ dragging = true;
2728
+ moved = false;
2729
+
2730
+ window.addEventListener("pointermove", onPointerMove);
2731
+ window.addEventListener("pointerup", onPointerUp);
2732
+ });
2733
+ }
2734
+
2735
+ renderShadowDom() {
2736
+ if (!this.shadow) return;
2737
+ this.shadow.innerHTML = "";
2738
+
2739
+ const styleEl = document.createElement("style");
2740
+ styleEl.textContent = WIDGET_SHADOW_STYLES;
2741
+ this.shadow.appendChild(styleEl);
2742
+
2743
+ const root = document.createElement("div");
2744
+ root.className = `cd-root pos-${this.config.position}`;
2745
+ this.rootContainer = root;
2746
+
2747
+ // 1. Compact Minimized Pinning Bar (visible while pinning elements on the page or minimized)
2748
+ const compactBar = document.createElement("div");
2749
+ compactBar.className = "cd-compact-bar";
2750
+ compactBar.setAttribute("role", "region");
2751
+ compactBar.setAttribute("aria-label", "ContextDrop Pinning Bar");
2752
+ compactBar.hidden = true;
2753
+ this.compactBarEl = compactBar;
2754
+
2755
+ const compactGrip = document.createElement("span");
2756
+ compactGrip.className = "cd-pin-dock-grip";
2757
+ compactGrip.title = "Drag to move pinning bar";
2758
+ compactGrip.innerHTML = ICON_GRIP;
2759
+ this.attachDragHandle(compactBar);
2760
+ compactBar.appendChild(compactGrip);
2761
+
2762
+ const compactPinBtn = document.createElement("button");
2763
+ compactPinBtn.type = "button";
2764
+ compactPinBtn.className = "cd-pin-corner-btn is-active";
2765
+ compactPinBtn.setAttribute("aria-label", "Toggle element pin mode");
2766
+ compactPinBtn.innerHTML = `${ICON_PIN}<span>Pinning</span>`;
2767
+ compactPinBtn.addEventListener("click", () => this.togglePinSelection());
2768
+ this.compactPinBtn = compactPinBtn;
2769
+ compactBar.appendChild(compactPinBtn);
2770
+
2771
+ const compactStatus = document.createElement("div");
2772
+ compactStatus.className = "cd-compact-status";
2773
+ const breadcrumbScroll = document.createElement("div");
2774
+ breadcrumbScroll.className = "cd-breadcrumb-scroll";
2775
+ this.compactBreadcrumbEl = breadcrumbScroll;
2776
+ compactStatus.appendChild(breadcrumbScroll);
2777
+ compactBar.appendChild(compactStatus);
2778
+
2779
+ const compactLayerUp = document.createElement("button");
2780
+ compactLayerUp.type = "button";
2781
+ compactLayerUp.className = "cd-icon-btn";
2782
+ compactLayerUp.title = "Step to outer/parent element (Up arrow)";
2783
+ compactLayerUp.setAttribute("aria-label", "Step to parent element");
2784
+ compactLayerUp.innerHTML = ICON_CHEVRON_UP;
2785
+ compactLayerUp.addEventListener("click", () => this.picker?.cycleDepth(1));
2786
+ compactBar.appendChild(compactLayerUp);
2787
+
2788
+ const compactLayerDown = document.createElement("button");
2789
+ compactLayerDown.type = "button";
2790
+ compactLayerDown.className = "cd-icon-btn";
2791
+ compactLayerDown.title = "Step to inner/child element (Down arrow)";
2792
+ compactLayerDown.setAttribute("aria-label", "Step to child element");
2793
+ compactLayerDown.innerHTML = ICON_CHEVRON_DOWN;
2794
+ compactLayerDown.addEventListener("click", () => this.picker?.cycleDepth(-1));
2795
+ compactBar.appendChild(compactLayerDown);
2796
+
2797
+ const expandBtn = document.createElement("button");
2798
+ expandBtn.type = "button";
2799
+ expandBtn.className = "cd-btn cd-btn-primary";
2800
+ expandBtn.setAttribute("aria-label", "Return to chatbox");
2801
+ expandBtn.innerHTML = `${ICON_EXPAND}<span>Chat</span>`;
2802
+ expandBtn.addEventListener("click", () => this.expandFromCompactBar());
2803
+ compactBar.appendChild(expandBtn);
2804
+
2805
+ // 2. Main Floating Chatbox Composer Window
2806
+ const composer = document.createElement("div");
2807
+ composer.className = "cd-composer";
2808
+ composer.setAttribute("role", "dialog");
2809
+ composer.setAttribute("aria-label", "ContextDrop Feedback Chatbox");
2810
+ composer.hidden = !this.isOpen;
2811
+ this.composerEl = composer;
2812
+
2813
+ // Drag-and-drop image support on the chatbox
2814
+ composer.addEventListener("dragover", (ev) => {
2815
+ ev.preventDefault();
2816
+ composer.classList.add("is-dragging-over");
2817
+ });
2818
+ composer.addEventListener("dragleave", () => {
2819
+ composer.classList.remove("is-dragging-over");
2820
+ });
2821
+ composer.addEventListener("drop", (ev) => {
2822
+ ev.preventDefault();
2823
+ composer.classList.remove("is-dragging-over");
2824
+ const files = ev.dataTransfer?.files;
2825
+ if (files && files.length > 0) {
2826
+ void this.handleFilesSelected(Array.from(files));
2827
+ }
2828
+ });
2829
+
2830
+ // Header with Top-Left Pin-Shaped Selector & Drag Handle
2831
+ const header = document.createElement("div");
2832
+ header.className = "cd-composer-header";
2833
+ this.attachDragHandle(header);
2834
+
2835
+ const headerLeft = document.createElement("div");
2836
+ headerLeft.className = "cd-header-left";
2837
+
2838
+ if (this.config.features.element_selection) {
2839
+ const cornerPinBtn = document.createElement("button");
2840
+ cornerPinBtn.type = "button";
2841
+ cornerPinBtn.className = "cd-pin-corner-btn";
2842
+ cornerPinBtn.title = "Pin any HTML element on the page as context";
2843
+ cornerPinBtn.setAttribute("aria-label", "Select DOM element on page");
2844
+ cornerPinBtn.innerHTML = `${ICON_PIN}<span>Pin Element</span>`;
2845
+ cornerPinBtn.addEventListener("click", () => this.startPinSelection());
2846
+ this.cornerPinBtn = cornerPinBtn;
2847
+ headerLeft.appendChild(cornerPinBtn);
2848
+ }
2849
+
2850
+ const title = document.createElement("h2");
2851
+ title.className = "cd-composer-title";
2852
+ title.textContent = "ContextDrop";
2853
+ headerLeft.appendChild(title);
2854
+
2855
+ const headerActions = document.createElement("div");
2856
+ headerActions.className = "cd-header-actions";
2857
+
2858
+ const minBtn = document.createElement("button");
2859
+ minBtn.type = "button";
2860
+ minBtn.className = "cd-icon-btn";
2861
+ minBtn.title = "Minimize to compact bar";
2862
+ minBtn.setAttribute("aria-label", "Minimize chatbox to compact bar");
2863
+ minBtn.innerHTML = ICON_MINIMIZE;
2864
+ minBtn.addEventListener("click", () => this.minimizeToCompactBar());
2865
+
2866
+ const closeBtn = document.createElement("button");
2867
+ closeBtn.type = "button";
2868
+ closeBtn.className = "cd-icon-btn";
2869
+ closeBtn.title = "Close chatbox (Esc)";
2870
+ closeBtn.setAttribute("aria-label", "Close feedback composer");
2871
+ closeBtn.innerHTML = ICON_CLOSE;
2872
+ closeBtn.addEventListener("click", () => this.closeComposer());
2873
+
2874
+ headerActions.appendChild(minBtn);
2875
+ headerActions.appendChild(closeBtn);
2876
+ header.appendChild(headerLeft);
2877
+ header.appendChild(headerActions);
2878
+ composer.appendChild(header);
2879
+
2880
+ // Conversational Chat Thread
2881
+ const chatThread = document.createElement("div");
2882
+ chatThread.className = "cd-chat-thread";
2883
+ chatThread.setAttribute("aria-live", "polite");
2884
+ this.chatThreadEl = chatThread;
2885
+ composer.appendChild(chatThread);
2886
+ this.renderChatThread();
2887
+
2888
+ // Pinned Elements & Attachments Chip Tray
2889
+ const tray = document.createElement("div");
2890
+ tray.className = "cd-tray";
2891
+ tray.setAttribute("aria-label", "Attached context items");
2892
+ this.trayEl = tray;
2893
+ composer.appendChild(tray);
2894
+
2895
+ // Status message bar
2896
+ const status = document.createElement("div");
2897
+ status.className = "cd-status";
2898
+ status.setAttribute("role", "status");
2899
+ status.hidden = true;
2900
+ this.statusEl = status;
2901
+ composer.appendChild(status);
2902
+
2903
+ // Hidden file input
2904
+ const fileInput = document.createElement("input");
2905
+ fileInput.type = "file";
2906
+ fileInput.multiple = true;
2907
+ fileInput.accept = this.config.limits.allowed_content_types.join(",");
2908
+ fileInput.hidden = true;
2909
+ fileInput.addEventListener("change", () => {
2910
+ if (fileInput.files) {
2911
+ void this.handleFilesSelected(Array.from(fileInput.files));
2912
+ fileInput.value = "";
2913
+ }
2914
+ });
2915
+ this.fileInputEl = fileInput;
2916
+ composer.appendChild(fileInput);
2917
+
2918
+ // Chatbox Prompt Input Shell
2919
+ const promptShell = document.createElement("div");
2920
+ promptShell.className = "cd-prompt-shell";
2921
+
2922
+ const textarea = document.createElement("textarea");
2923
+ textarea.className = "cd-textarea";
2924
+ textarea.placeholder = this.config.placeholder;
2925
+ textarea.setAttribute("aria-label", "Feedback description");
2926
+ textarea.addEventListener("keydown", (ev) => {
2927
+ if ((ev.metaKey || ev.ctrlKey) && ev.key === "Enter") {
2928
+ ev.preventDefault();
2929
+ void this.handleSubmit();
2930
+ } else if (ev.key === "Escape") {
2931
+ ev.preventDefault();
2932
+ this.closeComposer();
2933
+ }
2934
+ });
2935
+ textarea.addEventListener("paste", (ev) => this.handlePaste(ev));
2936
+ this.textareaEl = textarea;
2937
+ promptShell.appendChild(textarea);
2938
+
2939
+ // Toolbar inside prompt shell
2940
+ const toolbar = document.createElement("div");
2941
+ toolbar.className = "cd-toolbar";
2942
+
2943
+ const actions = document.createElement("div");
2944
+ actions.className = "cd-toolbar-actions";
2945
+
2946
+ if (this.config.features.element_selection) {
2947
+ const pickBtn = document.createElement("button");
2948
+ pickBtn.type = "button";
2949
+ pickBtn.className = "cd-btn";
2950
+ pickBtn.setAttribute("aria-label", "Pin HTML element as context");
2951
+ pickBtn.innerHTML = `${ICON_PIN}<span>Pin</span>`;
2952
+ pickBtn.addEventListener("click", () => this.startPinSelection());
2953
+ actions.appendChild(pickBtn);
2954
+ }
2955
+
2956
+ if (this.config.features.screenshot_capture) {
2957
+ const screenshotBtn = document.createElement("button");
2958
+ screenshotBtn.type = "button";
2959
+ screenshotBtn.className = "cd-btn";
2960
+ screenshotBtn.setAttribute("aria-label", "Capture viewport screenshot");
2961
+ screenshotBtn.innerHTML = `${ICON_CAMERA}<span>Screenshot</span>`;
2962
+ screenshotBtn.addEventListener("click", () => {
2963
+ void this.handleViewportScreenshot();
2964
+ });
2965
+ actions.appendChild(screenshotBtn);
2966
+ }
2967
+
2968
+ const uploadBtn = document.createElement("button");
2969
+ uploadBtn.type = "button";
2970
+ uploadBtn.className = "cd-btn";
2971
+ uploadBtn.setAttribute("aria-label", "Upload image attachment");
2972
+ uploadBtn.innerHTML = `${ICON_IMAGE}<span>Image</span>`;
2973
+ uploadBtn.addEventListener("click", () => this.fileInputEl?.click());
2974
+ actions.appendChild(uploadBtn);
2975
+
2976
+ const submitBtn = document.createElement("button");
2977
+ submitBtn.type = "button";
2978
+ submitBtn.className = "cd-btn cd-btn-primary";
2979
+ submitBtn.setAttribute("aria-label", "Submit feedback");
2980
+ submitBtn.innerHTML = `${ICON_SEND}<span>Send</span>`;
2981
+ submitBtn.addEventListener("click", () => {
2982
+ void this.handleSubmit();
2983
+ });
2984
+ this.submitBtn = submitBtn;
2985
+
2986
+ toolbar.appendChild(actions);
2987
+ toolbar.appendChild(submitBtn);
2988
+ promptShell.appendChild(toolbar);
2989
+ composer.appendChild(promptShell);
2990
+
2991
+ // 3. Freely Draggable Floating Pin Dock (visible when chatbox is closed, or draggable launcher)
2992
+ const pinDock = document.createElement("div");
2993
+ pinDock.className = "cd-pin-dock";
2994
+ this.pinDockEl = pinDock;
2995
+
2996
+ const dockGrip = document.createElement("span");
2997
+ dockGrip.className = "cd-pin-dock-grip";
2998
+ dockGrip.title = "Drag to float anywhere on screen";
2999
+ dockGrip.innerHTML = ICON_GRIP;
3000
+ this.attachDragHandle(dockGrip);
3001
+ pinDock.appendChild(dockGrip);
3002
+
3003
+ if (this.config.features.element_selection) {
3004
+ const floatingPinBtn = document.createElement("button");
3005
+ floatingPinBtn.type = "button";
3006
+ floatingPinBtn.className = "cd-floating-pin-btn";
3007
+ floatingPinBtn.title = "Pin an element on the page (or drag to move)";
3008
+ floatingPinBtn.setAttribute("aria-label", "Pin element on page");
3009
+ floatingPinBtn.innerHTML = `${ICON_PIN}<span>Pin</span>`;
3010
+ this.attachDragHandle(floatingPinBtn, () => this.startPinSelection());
3011
+ this.floatingPinBtn = floatingPinBtn;
3012
+ pinDock.appendChild(floatingPinBtn);
3013
+ }
3014
+
3015
+ const trigger = document.createElement("button");
3016
+ trigger.type = "button";
3017
+ trigger.className = "cd-trigger";
3018
+ trigger.setAttribute("aria-label", "Open feedback composer");
3019
+ trigger.setAttribute("aria-expanded", String(this.isOpen));
3020
+ trigger.innerHTML = `${ICON_CHAT}<span>ContextDrop</span>`;
3021
+ this.attachDragHandle(trigger, () => this.toggleComposer());
3022
+ this.triggerBtn = trigger;
3023
+ pinDock.appendChild(trigger);
3024
+
3025
+ root.appendChild(compactBar);
3026
+ root.appendChild(composer);
3027
+ root.appendChild(pinDock);
3028
+ this.shadow.appendChild(root);
3029
+
3030
+ this.renderCompactBreadcrumbs(null, []);
3031
+ this.syncUiStates();
3032
+ }
3033
+
3034
+ syncPinActiveIndicators(pickerActive ) {
3035
+ if (this.cornerPinBtn) {
3036
+ this.cornerPinBtn.classList.toggle("is-active", pickerActive);
3037
+ this.cornerPinBtn.setAttribute("aria-pressed", String(pickerActive));
3038
+ }
3039
+ if (this.floatingPinBtn) {
3040
+ this.floatingPinBtn.classList.toggle("is-active", pickerActive);
3041
+ this.floatingPinBtn.setAttribute("aria-pressed", String(pickerActive));
3042
+ }
3043
+ if (this.compactPinBtn) {
3044
+ this.compactPinBtn.classList.toggle("is-active", pickerActive);
3045
+ this.compactPinBtn.innerHTML = pickerActive
3046
+ ? `${ICON_PIN}<span>Pinning</span>`
3047
+ : `${ICON_PIN}<span>Pin Element</span>`;
3048
+ }
3049
+ }
3050
+
3051
+ syncUiStates() {
3052
+ const pickerActive = this.picker ? this.picker.isActive() : false;
3053
+ const totalContextCount = this.elementReferences.length + this.attachments.length;
3054
+
3055
+ if (this.composerEl) {
3056
+ this.composerEl.hidden = !this.isOpen || this.isMinimized;
3057
+ }
3058
+ if (this.compactBarEl) {
3059
+ this.compactBarEl.hidden = !(this.isOpen && this.isMinimized);
3060
+ }
3061
+ if (this.pinDockEl) {
3062
+ this.pinDockEl.hidden = this.isOpen;
3063
+ }
3064
+
3065
+ if (this.triggerBtn) {
3066
+ this.triggerBtn.setAttribute("aria-expanded", String(this.isOpen && !this.isMinimized));
3067
+ const countBadge =
3068
+ totalContextCount > 0 ? `<span class="cd-count-pill">${totalContextCount}</span>` : "";
3069
+ this.triggerBtn.innerHTML = `${ICON_CHAT}<span>ContextDrop</span>${countBadge}`;
3070
+ }
3071
+
3072
+ if (this.cornerPinBtn) {
3073
+ const pinCount =
3074
+ this.elementReferences.length > 0
3075
+ ? `<span class="cd-count-pill">${this.elementReferences.length}</span>`
3076
+ : "";
3077
+ this.cornerPinBtn.innerHTML = `${ICON_PIN}<span>Pin Element</span>${pinCount}`;
3078
+ }
3079
+
3080
+ this.syncPinActiveIndicators(pickerActive);
3081
+ }
3082
+
3083
+ renderCompactBreadcrumbs(
3084
+ activeCandidate ,
3085
+ ancestors
3086
+ ) {
3087
+ if (!this.compactBreadcrumbEl) return;
3088
+ this.compactBreadcrumbEl.innerHTML = "";
3089
+
3090
+ if (!activeCandidate || ancestors.length === 0) {
3091
+ const hint = document.createElement("span");
3092
+ hint.style.cssText = "font-size: 11.5px; color: #4b5262;";
3093
+ hint.textContent =
3094
+ this.elementReferences.length > 0
3095
+ ? `${this.elementReferences.length} pinned · Hover or click any element (scroll ↕ for layers)`
3096
+ : "Hover any element · Scroll ↕ for layers · Click to inspect stack";
3097
+ this.compactBreadcrumbEl.appendChild(hint);
3098
+ return;
3099
+ }
3100
+
3101
+ ancestors.forEach((ancEl, idx) => {
3102
+ if (idx > 0) {
3103
+ const sep = document.createElement("span");
3104
+ sep.className = "cd-crumb-sep";
3105
+ sep.innerHTML = ICON_CHEVRON_RIGHT;
3106
+ this.compactBreadcrumbEl?.appendChild(sep);
3107
+ }
3108
+
3109
+ const isCurrent = ancEl === activeCandidate.element;
3110
+ const crumb = document.createElement("button");
3111
+ crumb.type = "button";
3112
+ crumb.className = `cd-crumb-btn${isCurrent ? " is-current" : ""}`;
3113
+ crumb.textContent = computeSelector(ancEl);
3114
+ crumb.title = "Click to pin this element directly";
3115
+ crumb.addEventListener("mouseenter", () => {
3116
+ this.picker?.focusCandidateElement(ancEl);
3117
+ });
3118
+ crumb.addEventListener("click", () => {
3119
+ this.picker?.selectElementDirectly(ancEl);
3120
+ });
3121
+ this.compactBreadcrumbEl?.appendChild(crumb);
3122
+ });
3123
+ }
3124
+
3125
+ renderChatThread() {
3126
+ if (!this.chatThreadEl) return;
3127
+ this.chatThreadEl.innerHTML = "";
3128
+
3129
+ if (this.sessionHistory.length === 0) {
3130
+ const emptyBox = document.createElement("div");
3131
+ emptyBox.className = "cd-chat-empty";
3132
+
3133
+ const p1 = document.createElement("p");
3134
+ p1.className = "cd-chat-empty-text";
3135
+ p1.textContent = "Drop context directly from this page.";
3136
+
3137
+ const p2 = document.createElement("p");
3138
+ p2.className = "cd-chat-empty-hint";
3139
+ p2.textContent =
3140
+ "Use the top-left Pin selector to pick specific HTML elements (scroll or click to disambiguate overlapping layers), attach screenshots, and send your note.";
3141
+
3142
+ emptyBox.appendChild(p1);
3143
+ emptyBox.appendChild(p2);
3144
+ this.chatThreadEl.appendChild(emptyBox);
3145
+ return;
3146
+ }
3147
+
3148
+ for (const entry of this.sessionHistory) {
3149
+ const msgCard = document.createElement("div");
3150
+ msgCard.className = "cd-chat-msg";
3151
+
3152
+ if (entry.elements.length > 0 || entry.attachments.length > 0) {
3153
+ const pills = document.createElement("div");
3154
+ pills.className = "cd-chat-msg-pills";
3155
+
3156
+ for (const el of entry.elements) {
3157
+ const pill = document.createElement("span");
3158
+ pill.className = "cd-chat-msg-pill";
3159
+ pill.textContent = el.selector;
3160
+ pills.appendChild(pill);
3161
+ }
3162
+ for (const att of entry.attachments) {
3163
+ const pill = document.createElement("span");
3164
+ pill.className = "cd-chat-msg-pill";
3165
+ pill.textContent = att.label;
3166
+ pills.appendChild(pill);
3167
+ }
3168
+ msgCard.appendChild(pills);
3169
+ }
3170
+
3171
+ if (entry.message) {
3172
+ const textP = document.createElement("p");
3173
+ textP.className = "cd-chat-msg-text";
3174
+ textP.textContent = entry.message;
3175
+ msgCard.appendChild(textP);
3176
+ }
3177
+
3178
+ const meta = document.createElement("div");
3179
+ meta.className = "cd-chat-msg-meta";
3180
+ const idSpan = document.createElement("span");
3181
+ idSpan.textContent = `Dropped #${entry.id}`;
3182
+ const timeSpan = document.createElement("span");
3183
+ timeSpan.textContent = entry.createdAt;
3184
+ meta.appendChild(idSpan);
3185
+ meta.appendChild(timeSpan);
3186
+ msgCard.appendChild(meta);
3187
+
3188
+ this.chatThreadEl.appendChild(msgCard);
3189
+ }
3190
+
3191
+ this.chatThreadEl.scrollTop = this.chatThreadEl.scrollHeight;
3192
+ }
3193
+
3194
+ renderTray() {
3195
+ if (!this.trayEl) return;
3196
+ this.trayEl.innerHTML = "";
3197
+
3198
+ // Render ElementReference chips
3199
+ for (const elRef of this.elementReferences) {
3200
+ const chip = document.createElement("div");
3201
+ chip.className = "cd-chip";
3202
+ chip.setAttribute("data-chip-type", "element");
3203
+
3204
+ const domNode = this.elementDomNodes.get(elRef.client_ref);
3205
+ if (domNode) {
3206
+ chip.addEventListener("mouseenter", () => this.picker?.showExternalPreview(domNode));
3207
+ chip.addEventListener("mouseleave", () => this.picker?.hideExternalPreview());
3208
+ }
3209
+
3210
+ const badge = document.createElement("span");
3211
+ badge.className = "cd-chip-badge";
3212
+ badge.innerHTML = `${ICON_PIN}<span>Pin</span>`;
3213
+
3214
+ const label = document.createElement("span");
3215
+ label.className = "cd-chip-label";
3216
+ label.textContent = elRef.selector;
3217
+ label.title = elRef.dom_path || elRef.selector;
3218
+
3219
+ chip.appendChild(badge);
3220
+ chip.appendChild(label);
3221
+
3222
+ if (
3223
+ this.config.features.screenshot_capture &&
3224
+ domNode
3225
+ ) {
3226
+ const snapBtn = document.createElement("button");
3227
+ snapBtn.type = "button";
3228
+ snapBtn.className = "cd-chip-action";
3229
+ snapBtn.title = "Capture element screenshot";
3230
+ snapBtn.setAttribute("aria-label", `Capture screenshot of ${elRef.selector}`);
3231
+ snapBtn.innerHTML = ICON_CAMERA;
3232
+ snapBtn.addEventListener("click", () => {
3233
+ void this.handleElementScreenshot(domNode, elRef.client_ref, elRef.selector);
3234
+ });
3235
+ chip.appendChild(snapBtn);
3236
+ }
3237
+
3238
+ const removeBtn = document.createElement("button");
3239
+ removeBtn.type = "button";
3240
+ removeBtn.className = "cd-chip-remove";
3241
+ removeBtn.setAttribute("aria-label", `Remove ${elRef.selector}`);
3242
+ removeBtn.innerHTML = ICON_CLOSE;
3243
+ removeBtn.addEventListener("click", () => this.removeElementReference(elRef.client_ref));
3244
+
3245
+ chip.appendChild(removeBtn);
3246
+ this.trayEl.appendChild(chip);
3247
+ }
3248
+
3249
+ // Render Attachment chips (uploaded images & screenshots)
3250
+ for (const att of this.attachments) {
3251
+ const chip = document.createElement("div");
3252
+ chip.className = "cd-chip";
3253
+ chip.setAttribute("data-chip-type", att.attachment_type);
3254
+
3255
+ if (att.preview_url) {
3256
+ const img = document.createElement("img");
3257
+ img.className = "cd-chip-thumb";
3258
+ img.src = att.preview_url;
3259
+ img.alt = att.label;
3260
+ chip.appendChild(img);
3261
+ }
3262
+
3263
+ const badge = document.createElement("span");
3264
+ badge.className = "cd-chip-badge";
3265
+ badge.textContent = att.attachment_type === "screenshot" ? "Shot" : "Image";
3266
+
3267
+ const label = document.createElement("span");
3268
+ label.className = "cd-chip-label";
3269
+ label.textContent = att.label;
3270
+
3271
+ const removeBtn = document.createElement("button");
3272
+ removeBtn.type = "button";
3273
+ removeBtn.className = "cd-chip-remove";
3274
+ removeBtn.setAttribute("aria-label", `Remove ${att.label}`);
3275
+ removeBtn.innerHTML = ICON_CLOSE;
3276
+ removeBtn.addEventListener("click", () => this.removeAttachment(att.id));
3277
+
3278
+ chip.appendChild(badge);
3279
+ chip.appendChild(label);
3280
+ chip.appendChild(removeBtn);
3281
+ this.trayEl.appendChild(chip);
3282
+ }
3283
+ }
3284
+
3285
+ async handleFilesSelected(files ) {
3286
+ for (const file of files) {
3287
+ if (this.attachments.length >= this.config.limits.max_attachments) {
3288
+ this.showStatus(`Maximum of ${this.config.limits.max_attachments} attachments reached.`, "error");
3289
+ break;
3290
+ }
3291
+ if (!this.config.limits.allowed_content_types.includes(file.type)) {
3292
+ this.showStatus(`Unsupported file type: ${file.type || file.name}`, "error");
3293
+ continue;
3294
+ }
3295
+ if (file.size > this.config.limits.max_attachment_size) {
3296
+ this.showStatus(`File ${file.name} exceeds size limit.`, "error");
3297
+ continue;
3298
+ }
3299
+
3300
+ const dataUrl = await this.readFileAsDataUrl(file);
3301
+ this.attachments.push({
3302
+ id: `att_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
3303
+ attachment_type: "image",
3304
+ label: file.name,
3305
+ preview_url: dataUrl,
3306
+ blob: file,
3307
+ data_url: dataUrl,
3308
+ filename: file.name,
3309
+ metadata: {
3310
+ original_filename: file.name,
3311
+ byte_size: file.size
3312
+ }
3313
+ });
3314
+ }
3315
+ this.renderTray();
3316
+ this.syncUiStates();
3317
+ }
3318
+
3319
+ handlePaste(event ) {
3320
+ const items = event.clipboardData?.items;
3321
+ if (!items) return;
3322
+
3323
+ const imageFiles = [];
3324
+ for (let i = 0; i < items.length; i += 1) {
3325
+ const item = items[i];
3326
+ if (item.type.startsWith("image/")) {
3327
+ const file = item.getAsFile();
3328
+ if (file) imageFiles.push(file);
3329
+ }
3330
+ }
3331
+ if (imageFiles.length > 0) {
3332
+ void this.handleFilesSelected(imageFiles);
3333
+ }
3334
+ }
3335
+
3336
+ readFileAsDataUrl(file ) {
3337
+ return new Promise((resolve, reject) => {
3338
+ const reader = new FileReader();
3339
+ reader.onload = () => resolve(String(reader.result || ""));
3340
+ reader.onerror = () => reject(new Error("Failed to read image file"));
3341
+ reader.readAsDataURL(file);
3342
+ });
3343
+ }
3344
+
3345
+ async handleViewportScreenshot() {
3346
+ if (this.attachments.length >= this.config.limits.max_attachments) {
3347
+ this.showStatus(`Maximum of ${this.config.limits.max_attachments} attachments reached.`, "error");
3348
+ return;
3349
+ }
3350
+
3351
+ try {
3352
+ const result = await captureViewportScreenshot(this.config.privacy.excluded_selectors);
3353
+ this.attachments.push({
3354
+ id: `shot_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
3355
+ attachment_type: "screenshot",
3356
+ label: `Viewport (${result.width}×${result.height})`,
3357
+ preview_url: result.dataUrl,
3358
+ blob: result.blob,
3359
+ data_url: result.dataUrl,
3360
+ filename: `viewport-${Date.now()}.png`,
3361
+ metadata: result.metadata
3362
+ });
3363
+ this.renderTray();
3364
+ this.syncUiStates();
3365
+ } catch (err) {
3366
+ const message = err instanceof Error ? err.message : "Screenshot capture unavailable.";
3367
+ this.showStatus(message, "error");
3368
+ }
3369
+ }
3370
+
3371
+ async handleElementScreenshot(
3372
+ element ,
3373
+ elementClientRef ,
3374
+ selectorLabel
3375
+ ) {
3376
+ if (this.attachments.length >= this.config.limits.max_attachments) {
3377
+ this.showStatus(`Maximum of ${this.config.limits.max_attachments} attachments reached.`, "error");
3378
+ return;
3379
+ }
3380
+
3381
+ try {
3382
+ const result = await captureElementScreenshot(
3383
+ element,
3384
+ this.config.features.element_capture_padding,
3385
+ this.config.privacy.excluded_selectors,
3386
+ elementClientRef
3387
+ );
3388
+ this.attachments.push({
3389
+ id: `elshot_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
3390
+ attachment_type: "screenshot",
3391
+ label: `Element ${selectorLabel}`,
3392
+ preview_url: result.dataUrl,
3393
+ blob: result.blob,
3394
+ data_url: result.dataUrl,
3395
+ filename: `element-${Date.now()}.png`,
3396
+ element_reference_ref: elementClientRef,
3397
+ metadata: result.metadata
3398
+ });
3399
+ this.renderTray();
3400
+ this.syncUiStates();
3401
+ } catch (err) {
3402
+ const message = err instanceof Error ? err.message : "Element screenshot failed.";
3403
+ this.showStatus(message, "error");
3404
+ }
3405
+ }
3406
+
3407
+ async handleSubmit() {
3408
+ if (this.isSubmitting) return;
3409
+
3410
+ const message = (this.textareaEl?.value || "").trim();
3411
+ if (
3412
+ message.length === 0 &&
3413
+ this.attachments.length === 0 &&
3414
+ this.elementReferences.length === 0
3415
+ ) {
3416
+ this.showStatus("Enter a note, pin an element, or attach a screenshot first.", "error");
3417
+ return;
3418
+ }
3419
+
3420
+ this.isSubmitting = true;
3421
+ if (this.submitBtn) {
3422
+ this.submitBtn.disabled = true;
3423
+ this.submitBtn.innerHTML = `<span>Sending...</span>`;
3424
+ }
3425
+
3426
+ try {
3427
+ const submittedElements = [...this.elementReferences];
3428
+ const submittedAttachments = this.attachments.map((att) => ({
3429
+ id: att.id,
3430
+ label: att.label,
3431
+ preview_url: att.preview_url,
3432
+ attachment_type: att.attachment_type
3433
+ }));
3434
+
3435
+ const payload = {
3436
+ message,
3437
+ page_url: typeof window !== "undefined" ? window.location.href : "/",
3438
+ user_agent: typeof navigator !== "undefined" ? navigator.userAgent : "",
3439
+ metadata: {
3440
+ ...this.customMetadata,
3441
+ viewport: {
3442
+ width: typeof window !== "undefined" ? window.innerWidth : 0,
3443
+ height: typeof window !== "undefined" ? window.innerHeight : 0
3444
+ },
3445
+ locale: typeof navigator !== "undefined" ? navigator.language : "en"
3446
+ },
3447
+ element_references: this.elementReferences,
3448
+ attachments: this.attachments.map((att) => ({
3449
+ attachment_type: att.attachment_type,
3450
+ filename: att.filename,
3451
+ signed_blob_id: att.signed_blob_id,
3452
+ data_url: att.signed_blob_id ? undefined : att.data_url,
3453
+ element_reference_ref: att.element_reference_ref,
3454
+ metadata: att.metadata
3455
+ }))
3456
+ };
3457
+
3458
+ const res = await this.apiClient.submitFeedback(payload);
3459
+
3460
+ const now = new Date();
3461
+ const timeLabel = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
3462
+ this.sessionHistory.push({
3463
+ id: res?.id ?? this.sessionHistory.length + 1,
3464
+ message: message || "(Context drop without text)",
3465
+ createdAt: timeLabel,
3466
+ elements: submittedElements,
3467
+ attachments: submittedAttachments
3468
+ });
3469
+
3470
+ if (this.textareaEl) {
3471
+ this.textareaEl.value = "";
3472
+ }
3473
+ this.elementReferences = [];
3474
+ this.elementDomNodes.clear();
3475
+ this.attachments = [];
3476
+ this.picker?.hideExternalPreview();
3477
+ this.renderTray();
3478
+ this.renderChatThread();
3479
+ this.syncUiStates();
3480
+ this.showStatus("Context drop saved.", "success");
3481
+ } catch (err) {
3482
+ const msg = err instanceof Error ? err.message : "Failed to submit feedback.";
3483
+ this.showStatus(msg, "error");
3484
+ } finally {
3485
+ this.isSubmitting = false;
3486
+ if (this.submitBtn) {
3487
+ this.submitBtn.disabled = false;
3488
+ this.submitBtn.innerHTML = `${ICON_SEND}<span>Send</span>`;
3489
+ }
3490
+ }
3491
+ }
3492
+
3493
+ showStatus(text , kind ) {
3494
+ if (!this.statusEl) return;
3495
+ this.statusEl.textContent = text;
3496
+ this.statusEl.className = `cd-status ${kind}`;
3497
+ this.statusEl.hidden = false;
3498
+ }
3499
+ }
3500
+ function registerContextDropWidget() {
3501
+ if (typeof customElements !== "undefined" && !customElements.get("context-drop-widget")) {
3502
+ customElements.define("context-drop-widget", ContextDropWidgetElement);
3503
+ }
3504
+ }
3505
+
3506
+ // --- Module: index.ts ---
3507
+ registerContextDropWidget();
3508
+ })();