liteagents 2.5.2 → 2.6.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 (80) hide show
  1. package/CHANGELOG.md +76 -15
  2. package/README.md +58 -30
  3. package/docs/INSTALLATION_DEMO.md +1 -1
  4. package/docs/INSTALLATION_LOCATIONS.md +5 -5
  5. package/docs/INSTALLER_GUIDE.md +5 -5
  6. package/docs/KNOWLEDGE_BASE.md +5 -5
  7. package/docs/MIGRATION.md +1 -1
  8. package/docs/PASS_QUICK_START.md +1 -1
  9. package/docs/PRIVACY.md +1 -1
  10. package/docs/PUBLISHING.md +7 -7
  11. package/docs/RELEASE_NOTES_1.2.0.md +2 -2
  12. package/docs/SILENT_MODE_GUIDE.md +1 -1
  13. package/installer/cli.js +1 -1
  14. package/installer/installation-engine.js +82 -21
  15. package/installer/package-manager.js +34 -3
  16. package/package.json +7 -6
  17. package/packages/ampcode/AGENT.md +1 -0
  18. package/packages/ampcode/commands/friction/friction.js +19 -4
  19. package/packages/ampcode/commands/live-canvas/DESIGN_PRINCIPLES.md +2140 -0
  20. package/packages/ampcode/commands/live-canvas/README.md +273 -0
  21. package/packages/ampcode/commands/live-canvas/templates/.claude/settings.local.json +8 -0
  22. package/packages/ampcode/commands/live-canvas/templates/demo/post-variants.html +205 -0
  23. package/packages/ampcode/commands/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +1299 -0
  24. package/packages/ampcode/commands/live-canvas/templates/feedback-react/format-utils.ts +283 -0
  25. package/packages/ampcode/commands/live-canvas/templates/feedback-react/index.ts +62 -0
  26. package/packages/ampcode/commands/live-canvas/templates/feedback-react/selector-utils.ts +423 -0
  27. package/packages/ampcode/commands/live-canvas/templates/feedback-react/types.ts +118 -0
  28. package/packages/ampcode/commands/live-canvas/templates/overlay-vanilla.js +477 -0
  29. package/packages/ampcode/commands/live-canvas.md +1104 -0
  30. package/packages/claude/CLAUDE.md +1 -0
  31. package/packages/claude/commands/friction/friction.js +19 -4
  32. package/packages/claude/plugins/live-canvas-marketplace/.claude-plugin/marketplace.json +14 -0
  33. package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/.claude-plugin/plugin.json +18 -0
  34. package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/README.md +89 -0
  35. package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/package-lock.json +1142 -0
  36. package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/package.json +17 -0
  37. package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/schema.json +37 -0
  38. package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/server.js +179 -0
  39. package/packages/claude/plugins/live-canvas-marketplace/setup.sh +61 -0
  40. package/packages/claude/skills/live-canvas/DESIGN_PRINCIPLES.md +2140 -0
  41. package/packages/claude/skills/live-canvas/INTEGRATION_NOTES.md +210 -0
  42. package/packages/claude/skills/live-canvas/README.md +273 -0
  43. package/packages/claude/skills/live-canvas/SKILL.md +1119 -0
  44. package/packages/claude/skills/live-canvas/templates/.claude/settings.local.json +8 -0
  45. package/packages/claude/skills/live-canvas/templates/demo/post-variants.html +205 -0
  46. package/packages/claude/skills/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +1299 -0
  47. package/packages/claude/skills/live-canvas/templates/feedback-react/format-utils.ts +283 -0
  48. package/packages/claude/skills/live-canvas/templates/feedback-react/index.ts +62 -0
  49. package/packages/claude/skills/live-canvas/templates/feedback-react/selector-utils.ts +423 -0
  50. package/packages/claude/skills/live-canvas/templates/feedback-react/types.ts +118 -0
  51. package/packages/claude/skills/live-canvas/templates/overlay-vanilla.js +477 -0
  52. package/packages/claude/variants.json +2 -1
  53. package/packages/droid/AGENTS.md +1 -0
  54. package/packages/droid/commands/friction/friction.js +19 -4
  55. package/packages/droid/commands/live-canvas/DESIGN_PRINCIPLES.md +2140 -0
  56. package/packages/droid/commands/live-canvas/README.md +273 -0
  57. package/packages/droid/commands/live-canvas/templates/.claude/settings.local.json +8 -0
  58. package/packages/droid/commands/live-canvas/templates/demo/post-variants.html +205 -0
  59. package/packages/droid/commands/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +1299 -0
  60. package/packages/droid/commands/live-canvas/templates/feedback-react/format-utils.ts +283 -0
  61. package/packages/droid/commands/live-canvas/templates/feedback-react/index.ts +62 -0
  62. package/packages/droid/commands/live-canvas/templates/feedback-react/selector-utils.ts +423 -0
  63. package/packages/droid/commands/live-canvas/templates/feedback-react/types.ts +118 -0
  64. package/packages/droid/commands/live-canvas/templates/overlay-vanilla.js +477 -0
  65. package/packages/droid/commands/live-canvas.md +1104 -0
  66. package/packages/opencode/AGENTS.md +1 -0
  67. package/packages/opencode/command/friction/friction.js +19 -4
  68. package/packages/opencode/command/live-canvas/DESIGN_PRINCIPLES.md +2140 -0
  69. package/packages/opencode/command/live-canvas/README.md +273 -0
  70. package/packages/opencode/command/live-canvas/templates/.claude/settings.local.json +8 -0
  71. package/packages/opencode/command/live-canvas/templates/demo/post-variants.html +205 -0
  72. package/packages/opencode/command/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +1299 -0
  73. package/packages/opencode/command/live-canvas/templates/feedback-react/format-utils.ts +283 -0
  74. package/packages/opencode/command/live-canvas/templates/feedback-react/index.ts +62 -0
  75. package/packages/opencode/command/live-canvas/templates/feedback-react/selector-utils.ts +423 -0
  76. package/packages/opencode/command/live-canvas/templates/feedback-react/types.ts +118 -0
  77. package/packages/opencode/command/live-canvas/templates/overlay-vanilla.js +477 -0
  78. package/packages/opencode/command/live-canvas.md +1104 -0
  79. package/packages/opencode/opencode.jsonc +4 -0
  80. package/packages/subagentic-manual.md +18 -15
@@ -0,0 +1,1299 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Design Lab Interactive Feedback System - FeedbackOverlay Component
5
+ *
6
+ * A SELF-CONTAINED React component that enables Figma-like commenting
7
+ * on design variants. All types, utilities, and styles are inline for
8
+ * maximum reliability across different project configurations.
9
+ *
10
+ * USAGE: Copy this single file to your route directory and import it.
11
+ * No other dependencies needed beyond React.
12
+ *
13
+ * Features:
14
+ * - Toggle button to enter/exit feedback mode
15
+ * - Click-to-comment on any element
16
+ * - Numbered pins at comment locations
17
+ * - Sidebar with all comments grouped by variant
18
+ * - Submit modal with clipboard copy (paste into terminal for Claude)
19
+ */
20
+
21
+ import React, {
22
+ useState,
23
+ useEffect,
24
+ useCallback,
25
+ useRef,
26
+ type CSSProperties,
27
+ } from 'react';
28
+ import { createPortal } from 'react-dom';
29
+
30
+ // ============================================================================
31
+ // Types (inlined for portability)
32
+ // ============================================================================
33
+
34
+ type VariantId = 'A' | 'B' | 'C' | 'D' | 'E' | 'F';
35
+
36
+ interface ElementIdentifier {
37
+ selector: string;
38
+ readablePath: string;
39
+ tagName: string;
40
+ textContent: string;
41
+ className: string;
42
+ attributes: Record<string, string>;
43
+ }
44
+
45
+ interface Coordinates {
46
+ x: number;
47
+ y: number;
48
+ }
49
+
50
+ interface Comment {
51
+ id: string;
52
+ variant: VariantId;
53
+ element: ElementIdentifier;
54
+ coordinates: Coordinates;
55
+ text: string;
56
+ timestamp: number;
57
+ }
58
+
59
+ interface FeedbackPayload {
60
+ version: '1.0';
61
+ target: string;
62
+ timestamp: string;
63
+ comments: Comment[];
64
+ overall: string;
65
+ }
66
+
67
+ interface FeedbackOverlayProps {
68
+ targetName: string;
69
+ variants?: VariantId[];
70
+ onSubmit?: (payload: FeedbackPayload) => void;
71
+ }
72
+
73
+ interface FeedbackState {
74
+ isActive: boolean;
75
+ selectedElement: HTMLElement | null;
76
+ panelPosition: { x: number; y: number } | null;
77
+ comments: Comment[];
78
+ currentCommentText: string;
79
+ overallDirection: string;
80
+ showSubmitModal: boolean;
81
+ formattedOutput: string;
82
+ }
83
+
84
+ const STORAGE_KEY = 'design-lab-feedback';
85
+ const OVERLAY_CLASS_PREFIX = 'dl-feedback';
86
+
87
+ // CSS isolation styles injected into head to override page styles with !important
88
+ const ISOLATION_STYLES = `
89
+ .${OVERLAY_CLASS_PREFIX}-textarea {
90
+ all: revert !important;
91
+ box-sizing: border-box !important;
92
+ width: 100% !important;
93
+ min-height: 80px !important;
94
+ padding: 12px !important;
95
+ font-size: 14px !important;
96
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
97
+ color: #1f2937 !important;
98
+ background-color: #ffffff !important;
99
+ caret-color: #1f2937 !important;
100
+ border: 1px solid #d1d5db !important;
101
+ border-radius: 8px !important;
102
+ resize: vertical !important;
103
+ outline: none !important;
104
+ line-height: 1.5 !important;
105
+ }
106
+ .${OVERLAY_CLASS_PREFIX}-textarea:focus {
107
+ border-color: #6366f1 !important;
108
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2) !important;
109
+ }
110
+ .${OVERLAY_CLASS_PREFIX}-textarea::placeholder {
111
+ color: #9ca3af !important;
112
+ opacity: 1 !important;
113
+ }
114
+ .${OVERLAY_CLASS_PREFIX}-overall-textarea {
115
+ all: revert !important;
116
+ box-sizing: border-box !important;
117
+ width: 100% !important;
118
+ min-height: 100px !important;
119
+ padding: 12px !important;
120
+ font-size: 14px !important;
121
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
122
+ color: #1f2937 !important;
123
+ background-color: #ffffff !important;
124
+ caret-color: #1f2937 !important;
125
+ border: 1px solid #d1d5db !important;
126
+ border-radius: 8px !important;
127
+ resize: vertical !important;
128
+ outline: none !important;
129
+ line-height: 1.5 !important;
130
+ }
131
+ .${OVERLAY_CLASS_PREFIX}-overall-textarea:focus {
132
+ border-color: #6366f1 !important;
133
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2) !important;
134
+ }
135
+ .${OVERLAY_CLASS_PREFIX}-overall-textarea::placeholder {
136
+ color: #9ca3af !important;
137
+ opacity: 1 !important;
138
+ }
139
+ `;
140
+
141
+ // ============================================================================
142
+ // Utilities (inlined for portability)
143
+ // ============================================================================
144
+
145
+ const CSS_IN_JS_PATTERN = /^(css|sc|emotion|styled|chakra|mui|ant)-[a-z0-9]+$/i;
146
+ const HASH_SUFFIX_PATTERN = /_[a-z0-9]{5,}$/i;
147
+
148
+ function isCssInJsClass(className: string): boolean {
149
+ return (
150
+ CSS_IN_JS_PATTERN.test(className) ||
151
+ HASH_SUFFIX_PATTERN.test(className) ||
152
+ /^[a-z]{1,3}[0-9]{4,}$/i.test(className)
153
+ );
154
+ }
155
+
156
+ function filterClasses(classNames: string): string {
157
+ if (!classNames) return '';
158
+ return classNames
159
+ .split(/\s+/)
160
+ .filter((cls) => cls && !isCssInJsClass(cls))
161
+ .join(' ');
162
+ }
163
+
164
+ function getRelevantAttributes(element: HTMLElement): Record<string, string> {
165
+ const attrs: Record<string, string> = {};
166
+ const relevantPrefixes = ['data-', 'aria-'];
167
+ for (const attr of Array.from(element.attributes)) {
168
+ if (
169
+ relevantPrefixes.some((prefix) => attr.name.startsWith(prefix)) ||
170
+ attr.name === 'id' ||
171
+ attr.name === 'role' ||
172
+ attr.name === 'type' ||
173
+ attr.name === 'name'
174
+ ) {
175
+ attrs[attr.name] = attr.value;
176
+ }
177
+ }
178
+ return attrs;
179
+ }
180
+
181
+ function generateStructuralPath(element: HTMLElement, variantRoot: HTMLElement): string {
182
+ const path: string[] = [];
183
+ let current: HTMLElement | null = element;
184
+ while (current && current !== variantRoot && current.parentElement) {
185
+ const parent = current.parentElement;
186
+ const siblings = Array.from(parent.children).filter(
187
+ (child) => child.tagName === current!.tagName
188
+ );
189
+ const tag = current.tagName.toLowerCase();
190
+ if (siblings.length === 1) {
191
+ path.unshift(tag);
192
+ } else {
193
+ const index = siblings.indexOf(current) + 1;
194
+ path.unshift(`${tag}:nth-child(${index})`);
195
+ }
196
+ current = parent;
197
+ if (path.length >= 4) break;
198
+ }
199
+ return path.join(' > ');
200
+ }
201
+
202
+ function generateSelector(element: HTMLElement, variantRoot: HTMLElement): string {
203
+ const testId = element.getAttribute('data-testid') || element.getAttribute('data-cy');
204
+ if (testId) return `[data-testid="${testId}"]`;
205
+
206
+ const id = element.id;
207
+ if (id && !isCssInJsClass(id) && !id.startsWith(':')) return `#${id}`;
208
+
209
+ const ariaLabel = element.getAttribute('aria-label');
210
+ if (ariaLabel) {
211
+ const tag = element.tagName.toLowerCase();
212
+ return `${tag}[aria-label="${ariaLabel}"]`;
213
+ }
214
+
215
+ const role = element.getAttribute('role');
216
+ if (role && !['generic', 'presentation', 'none'].includes(role)) {
217
+ const tag = element.tagName.toLowerCase();
218
+ const name = element.getAttribute('name');
219
+ if (name) return `${tag}[role="${role}"][name="${name}"]`;
220
+ return `${tag}[role="${role}"]`;
221
+ }
222
+
223
+ const meaningfulClasses = filterClasses(element.className);
224
+ if (meaningfulClasses) {
225
+ const classSelector = meaningfulClasses
226
+ .split(/\s+/)
227
+ .slice(0, 2)
228
+ .map((c) => `.${c}`)
229
+ .join('');
230
+ const matches = variantRoot.querySelectorAll(classSelector);
231
+ if (matches.length === 1) return classSelector;
232
+
233
+ const parent = element.parentElement;
234
+ if (parent && parent !== variantRoot) {
235
+ const parentClasses = filterClasses(parent.className);
236
+ if (parentClasses) {
237
+ const parentSelector = parentClasses.split(/\s+/).slice(0, 1).map((c) => `.${c}`).join('');
238
+ return `${parentSelector} > ${classSelector}`;
239
+ }
240
+ }
241
+ return classSelector;
242
+ }
243
+
244
+ const tag = element.tagName.toLowerCase();
245
+ const type = element.getAttribute('type');
246
+ const name = element.getAttribute('name');
247
+ if (tag === 'input' || tag === 'button' || tag === 'select') {
248
+ if (name) return `${tag}[name="${name}"]`;
249
+ if (type) return `${tag}[type="${type}"]`;
250
+ }
251
+
252
+ return generateStructuralPath(element, variantRoot);
253
+ }
254
+
255
+ const tagLabels: Record<string, string> = {
256
+ div: 'Container', section: 'Section', article: 'Article', nav: 'Navigation',
257
+ header: 'Header', footer: 'Footer', main: 'Main', aside: 'Sidebar',
258
+ form: 'Form', ul: 'List', ol: 'Numbered List', li: 'List Item',
259
+ button: 'Button', a: 'Link', input: 'Input', select: 'Dropdown',
260
+ textarea: 'Text Area', img: 'Image', span: 'Text', p: 'Paragraph',
261
+ };
262
+
263
+ function capitalizeTag(tag: string): string {
264
+ return tagLabels[tag] || tag.charAt(0).toUpperCase() + tag.slice(1);
265
+ }
266
+
267
+ function formatClassName(className: string): string {
268
+ return className
269
+ .replace(/[-_]/g, ' ')
270
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
271
+ .replace(/\b\w/g, (c) => c.toUpperCase());
272
+ }
273
+
274
+ function getElementLabel(element: HTMLElement): string {
275
+ const tag = element.tagName.toLowerCase();
276
+ const ariaLabel = element.getAttribute('aria-label');
277
+ if (ariaLabel) return ariaLabel;
278
+
279
+ if (tag === 'button' || tag === 'a') {
280
+ const text = element.textContent?.trim().slice(0, 30);
281
+ if (text) return `${capitalizeTag(tag)}: "${text}"`;
282
+ }
283
+
284
+ if (tag === 'input' || tag === 'select' || tag === 'textarea') {
285
+ const name = element.getAttribute('name') || element.getAttribute('placeholder');
286
+ if (name) return `${capitalizeTag(tag)}: ${name}`;
287
+ }
288
+
289
+ if (/^h[1-6]$/.test(tag)) {
290
+ const text = element.textContent?.trim().slice(0, 30);
291
+ if (text) return `Heading: "${text}"`;
292
+ }
293
+
294
+ const classes = filterClasses(element.className);
295
+ if (classes) return formatClassName(classes.split(/\s+/)[0]);
296
+
297
+ return capitalizeTag(tag);
298
+ }
299
+
300
+ function generateReadablePath(element: HTMLElement, variantRoot: HTMLElement, variantId: VariantId): string {
301
+ const parts: string[] = [`Variant ${variantId}`];
302
+ const path: HTMLElement[] = [];
303
+ let current: HTMLElement | null = element;
304
+ while (current && current !== variantRoot) {
305
+ path.unshift(current);
306
+ current = current.parentElement;
307
+ }
308
+ const relevantPath = path.slice(-3);
309
+ for (const el of relevantPath) {
310
+ const label = getElementLabel(el);
311
+ if (label) parts.push(label);
312
+ }
313
+ return parts.join(' > ');
314
+ }
315
+
316
+ function getTextContent(element: HTMLElement, maxLength = 50): string {
317
+ const text = element.textContent?.trim() || '';
318
+ if (text.length <= maxLength) return text;
319
+ return text.slice(0, maxLength - 3) + '...';
320
+ }
321
+
322
+ function calculateCoordinates(element: HTMLElement, variantRoot: HTMLElement, clickX: number, clickY: number): Coordinates {
323
+ const rootRect = variantRoot.getBoundingClientRect();
324
+ const x = ((clickX - rootRect.left) / rootRect.width) * 100;
325
+ const y = ((clickY - rootRect.top) / rootRect.height) * 100;
326
+ return { x: Math.round(x * 10) / 10, y: Math.round(y * 10) / 10 };
327
+ }
328
+
329
+ function identifyElement(
330
+ element: HTMLElement,
331
+ variantRoot: HTMLElement,
332
+ variantId: VariantId,
333
+ clickX: number,
334
+ clickY: number
335
+ ): { element: ElementIdentifier; coordinates: Coordinates } {
336
+ return {
337
+ element: {
338
+ selector: generateSelector(element, variantRoot),
339
+ readablePath: generateReadablePath(element, variantRoot, variantId),
340
+ tagName: element.tagName.toLowerCase(),
341
+ textContent: getTextContent(element),
342
+ className: filterClasses(element.className),
343
+ attributes: getRelevantAttributes(element),
344
+ },
345
+ coordinates: calculateCoordinates(element, variantRoot, clickX, clickY),
346
+ };
347
+ }
348
+
349
+ function isOverlayElement(element: HTMLElement): boolean {
350
+ let current: HTMLElement | null = element;
351
+ while (current) {
352
+ if (current.className && typeof current.className === 'string' && current.className.includes(OVERLAY_CLASS_PREFIX)) {
353
+ return true;
354
+ }
355
+ current = current.parentElement;
356
+ }
357
+ return false;
358
+ }
359
+
360
+ function findVariantContainer(element: HTMLElement): { root: HTMLElement; variantId: VariantId } | null {
361
+ let current: HTMLElement | null = element;
362
+ while (current) {
363
+ const variantId = current.getAttribute('data-variant');
364
+ if (variantId && /^[A-F]$/.test(variantId)) {
365
+ return { root: current, variantId: variantId as VariantId };
366
+ }
367
+ current = current.parentElement;
368
+ }
369
+ return null;
370
+ }
371
+
372
+ function groupByVariant(comments: Comment[]): Map<VariantId, Comment[]> {
373
+ const groups = new Map<VariantId, Comment[]>();
374
+ for (const comment of comments) {
375
+ const existing = groups.get(comment.variant) || [];
376
+ existing.push(comment);
377
+ groups.set(comment.variant, existing);
378
+ }
379
+ return new Map([...groups.entries()].sort(([a], [b]) => a.localeCompare(b)));
380
+ }
381
+
382
+ function formatComment(comment: Comment, index: number): string {
383
+ const { element, text } = comment;
384
+ const selectorPart = `\`${element.selector}\``;
385
+ const textHint = element.textContent ? `, ${element.tagName} with "${element.textContent}"` : '';
386
+ return `${index}. **${element.readablePath.split(' > ').pop()}** (${selectorPart}${textHint})
387
+ "${text}"`;
388
+ }
389
+
390
+ function formatAsMarkdown(comments: Comment[], target: string, overall: string): string {
391
+ const lines: string[] = [];
392
+ lines.push('## Design Lab Feedback');
393
+ lines.push('');
394
+ lines.push(`**Target:** ${target}`);
395
+ lines.push(`**Comments:** ${comments.length}`);
396
+ lines.push('');
397
+
398
+ const grouped = groupByVariant(comments);
399
+ for (const [variantId, variantComments] of grouped) {
400
+ lines.push(`### Variant ${variantId}`);
401
+ variantComments.forEach((comment, index) => {
402
+ lines.push(formatComment(comment, index + 1));
403
+ lines.push('');
404
+ });
405
+ }
406
+
407
+ if (overall.trim()) {
408
+ lines.push('### Overall Direction');
409
+ lines.push(overall.trim());
410
+ lines.push('');
411
+ }
412
+
413
+ lines.push('---');
414
+ lines.push('*Feedback from Design Lab interactive overlay*');
415
+ return lines.join('\n');
416
+ }
417
+
418
+ async function copyToClipboard(text: string): Promise<boolean> {
419
+ if (navigator.clipboard && navigator.clipboard.writeText) {
420
+ try {
421
+ await navigator.clipboard.writeText(text);
422
+ return true;
423
+ } catch (err) {
424
+ console.warn('Clipboard API failed, trying fallback:', err);
425
+ }
426
+ }
427
+ try {
428
+ const textarea = document.createElement('textarea');
429
+ textarea.value = text;
430
+ textarea.style.position = 'fixed';
431
+ textarea.style.left = '-9999px';
432
+ textarea.style.top = '0';
433
+ textarea.setAttribute('readonly', '');
434
+ document.body.appendChild(textarea);
435
+ textarea.select();
436
+ textarea.setSelectionRange(0, text.length);
437
+ const success = document.execCommand('copy');
438
+ document.body.removeChild(textarea);
439
+ return success;
440
+ } catch (err) {
441
+ console.error('Fallback clipboard method failed:', err);
442
+ return false;
443
+ }
444
+ }
445
+
446
+ function generateId(): string {
447
+ return `comment-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
448
+ }
449
+
450
+ function validateFeedback(comments: Comment[], overall: string): { valid: boolean; errors: string[] } {
451
+ const errors: string[] = [];
452
+ if (comments.length === 0) errors.push('Please add at least one comment to an element.');
453
+ if (!overall.trim()) errors.push('Please provide an overall direction for the design.');
454
+ return { valid: errors.length === 0, errors };
455
+ }
456
+
457
+ // ============================================================================
458
+ // Inline Styles (no external CSS dependencies)
459
+ // ============================================================================
460
+
461
+ const styles: Record<string, CSSProperties> = {
462
+ toggleButton: {
463
+ position: 'fixed',
464
+ bottom: '24px',
465
+ right: '24px',
466
+ zIndex: 10000,
467
+ display: 'flex',
468
+ alignItems: 'center',
469
+ gap: '8px',
470
+ padding: '12px 20px',
471
+ fontSize: '14px',
472
+ fontWeight: 600,
473
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
474
+ color: '#fff',
475
+ backgroundColor: '#6366f1',
476
+ border: 'none',
477
+ borderRadius: '9999px',
478
+ cursor: 'pointer',
479
+ boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
480
+ transition: 'all 150ms ease',
481
+ },
482
+ toggleButtonActive: { backgroundColor: '#ef4444' },
483
+ toggleButtonHover: {
484
+ transform: 'scale(1.05)',
485
+ boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
486
+ },
487
+ hoverHighlight: {
488
+ position: 'fixed',
489
+ pointerEvents: 'none',
490
+ border: '2px dashed #6366f1',
491
+ backgroundColor: 'rgba(99, 102, 241, 0.1)',
492
+ borderRadius: '4px',
493
+ zIndex: 9998,
494
+ transition: 'all 100ms ease',
495
+ },
496
+ pin: {
497
+ position: 'absolute',
498
+ width: '28px',
499
+ height: '28px',
500
+ backgroundColor: '#6366f1',
501
+ color: '#fff',
502
+ borderRadius: '50%',
503
+ display: 'flex',
504
+ alignItems: 'center',
505
+ justifyContent: 'center',
506
+ fontSize: '12px',
507
+ fontWeight: 700,
508
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
509
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.2)',
510
+ cursor: 'pointer',
511
+ zIndex: 9999,
512
+ transform: 'translate(-50%, -50%)',
513
+ transition: 'transform 150ms ease',
514
+ },
515
+ // Note: Comment panel styles removed - comment input is now in sidebar
516
+ buttonPrimary: {
517
+ flex: 1,
518
+ padding: '10px 16px',
519
+ fontSize: '14px',
520
+ fontWeight: 600,
521
+ color: '#fff',
522
+ backgroundColor: '#6366f1',
523
+ border: 'none',
524
+ borderRadius: '8px',
525
+ cursor: 'pointer',
526
+ transition: 'background-color 150ms ease',
527
+ },
528
+ buttonSecondary: {
529
+ padding: '10px 16px',
530
+ fontSize: '14px',
531
+ fontWeight: 500,
532
+ color: '#374151',
533
+ backgroundColor: '#fff',
534
+ border: '1px solid #d1d5db',
535
+ borderRadius: '8px',
536
+ cursor: 'pointer',
537
+ transition: 'background-color 150ms ease',
538
+ },
539
+ sidebar: {
540
+ position: 'fixed',
541
+ top: 0,
542
+ right: 0,
543
+ width: '360px',
544
+ height: '100vh',
545
+ backgroundColor: '#fff',
546
+ borderLeft: '1px solid #e5e7eb',
547
+ zIndex: 9997,
548
+ display: 'flex',
549
+ flexDirection: 'column',
550
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
551
+ boxShadow: '-4px 0 20px rgba(0, 0, 0, 0.05)',
552
+ },
553
+ sidebarHeader: {
554
+ padding: '20px',
555
+ borderBottom: '1px solid #e5e7eb',
556
+ backgroundColor: '#f9fafb',
557
+ },
558
+ sidebarTitle: { fontSize: '18px', fontWeight: 700, color: '#111827', margin: 0 },
559
+ sidebarSubtitle: { fontSize: '13px', color: '#6b7280', marginTop: '4px' },
560
+ sidebarContent: { flex: 1, overflowY: 'auto', padding: '16px' },
561
+ sidebarSection: { marginBottom: '20px' },
562
+ sidebarSectionTitle: {
563
+ fontSize: '12px',
564
+ fontWeight: 600,
565
+ color: '#6b7280',
566
+ textTransform: 'uppercase',
567
+ letterSpacing: '0.05em',
568
+ marginBottom: '12px',
569
+ },
570
+ commentCard: {
571
+ padding: '12px',
572
+ backgroundColor: '#f9fafb',
573
+ borderRadius: '8px',
574
+ marginBottom: '8px',
575
+ border: '1px solid #e5e7eb',
576
+ },
577
+ commentCardHeader: { display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' },
578
+ commentCardPin: {
579
+ width: '24px',
580
+ height: '24px',
581
+ backgroundColor: '#6366f1',
582
+ color: '#fff',
583
+ borderRadius: '50%',
584
+ display: 'flex',
585
+ alignItems: 'center',
586
+ justifyContent: 'center',
587
+ fontSize: '11px',
588
+ fontWeight: 700,
589
+ flexShrink: 0,
590
+ },
591
+ commentCardElement: {
592
+ fontSize: '13px',
593
+ fontWeight: 500,
594
+ color: '#374151',
595
+ overflow: 'hidden',
596
+ textOverflow: 'ellipsis',
597
+ whiteSpace: 'nowrap',
598
+ },
599
+ commentCardText: { fontSize: '14px', color: '#4b5563', lineHeight: 1.5 },
600
+ commentCardDelete: {
601
+ marginLeft: 'auto',
602
+ padding: '4px 8px',
603
+ fontSize: '12px',
604
+ color: '#ef4444',
605
+ background: 'none',
606
+ border: 'none',
607
+ cursor: 'pointer',
608
+ borderRadius: '4px',
609
+ transition: 'background-color 150ms ease',
610
+ },
611
+ overallSection: {
612
+ padding: '16px',
613
+ borderTop: '1px solid #e5e7eb',
614
+ backgroundColor: '#f9fafb',
615
+ },
616
+ overallLabel: { fontSize: '13px', fontWeight: 600, color: '#374151', marginBottom: '8px' },
617
+ // Note: overallTextarea styles moved to CSS injection for !important isolation
618
+ submitSection: { padding: '16px', borderTop: '1px solid #e5e7eb' },
619
+ submitButton: {
620
+ width: '100%',
621
+ padding: '14px 20px',
622
+ fontSize: '15px',
623
+ fontWeight: 600,
624
+ color: '#fff',
625
+ backgroundColor: '#10b981',
626
+ border: 'none',
627
+ borderRadius: '10px',
628
+ cursor: 'pointer',
629
+ transition: 'background-color 150ms ease',
630
+ },
631
+ submitButtonDisabled: { backgroundColor: '#9ca3af', cursor: 'not-allowed' },
632
+ modalOverlay: {
633
+ position: 'fixed',
634
+ inset: 0,
635
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
636
+ zIndex: 10002,
637
+ display: 'flex',
638
+ alignItems: 'center',
639
+ justifyContent: 'center',
640
+ padding: '20px',
641
+ },
642
+ modal: {
643
+ width: '100%',
644
+ maxWidth: '600px',
645
+ maxHeight: '80vh',
646
+ backgroundColor: '#fff',
647
+ borderRadius: '16px',
648
+ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
649
+ display: 'flex',
650
+ flexDirection: 'column',
651
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
652
+ },
653
+ modalHeader: {
654
+ padding: '20px 24px',
655
+ borderBottom: '1px solid #e5e7eb',
656
+ display: 'flex',
657
+ alignItems: 'center',
658
+ justifyContent: 'space-between',
659
+ },
660
+ modalTitle: { fontSize: '18px', fontWeight: 700, color: '#111827', margin: 0 },
661
+ modalClose: {
662
+ padding: '8px',
663
+ background: 'none',
664
+ border: 'none',
665
+ cursor: 'pointer',
666
+ color: '#6b7280',
667
+ borderRadius: '8px',
668
+ transition: 'background-color 150ms ease',
669
+ },
670
+ modalBody: { flex: 1, overflowY: 'auto', padding: '20px 24px' },
671
+ modalPreview: {
672
+ padding: '16px',
673
+ backgroundColor: '#f9fafb',
674
+ borderRadius: '8px',
675
+ border: '1px solid #e5e7eb',
676
+ fontSize: '13px',
677
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace',
678
+ whiteSpace: 'pre-wrap',
679
+ color: '#374151',
680
+ maxHeight: '300px',
681
+ overflowY: 'auto',
682
+ },
683
+ modalFooter: {
684
+ padding: '16px 24px',
685
+ borderTop: '1px solid #e5e7eb',
686
+ display: 'flex',
687
+ gap: '12px',
688
+ justifyContent: 'flex-end',
689
+ },
690
+ successMessage: {
691
+ padding: '12px 16px',
692
+ backgroundColor: '#d1fae5',
693
+ color: '#065f46',
694
+ borderRadius: '8px',
695
+ fontSize: '14px',
696
+ fontWeight: 500,
697
+ marginBottom: '16px',
698
+ display: 'flex',
699
+ alignItems: 'center',
700
+ gap: '8px',
701
+ },
702
+ errorMessage: {
703
+ padding: '12px 16px',
704
+ backgroundColor: '#fee2e2',
705
+ color: '#991b1b',
706
+ borderRadius: '8px',
707
+ fontSize: '14px',
708
+ marginBottom: '16px',
709
+ },
710
+ };
711
+
712
+ // ============================================================================
713
+ // Component Implementation
714
+ // ============================================================================
715
+
716
+ export function FeedbackOverlay({
717
+ targetName,
718
+ variants = ['A', 'B', 'C', 'D', 'E'],
719
+ onSubmit,
720
+ }: FeedbackOverlayProps) {
721
+ // Load initial state from localStorage (lazy initialization)
722
+ const [state, setState] = useState<FeedbackState>(() => {
723
+ const defaultState: FeedbackState = {
724
+ isActive: false,
725
+ selectedElement: null,
726
+ panelPosition: null,
727
+ comments: [],
728
+ currentCommentText: '',
729
+ overallDirection: '',
730
+ showSubmitModal: false,
731
+ formattedOutput: '',
732
+ };
733
+
734
+ if (typeof window === 'undefined') return defaultState;
735
+
736
+ try {
737
+ const saved = localStorage.getItem(STORAGE_KEY);
738
+ if (saved) {
739
+ const data = JSON.parse(saved);
740
+ return {
741
+ ...defaultState,
742
+ comments: data.comments || [],
743
+ overallDirection: data.overallDirection || '',
744
+ };
745
+ }
746
+ } catch (err) {
747
+ console.warn('Failed to load saved feedback:', err);
748
+ }
749
+ return defaultState;
750
+ });
751
+
752
+ const [, setHoveredElement] = useState<HTMLElement | null>(null);
753
+ const [hoverRect, setHoverRect] = useState<DOMRect | null>(null);
754
+ const [copySuccess, setCopySuccess] = useState(false);
755
+ const [validationErrors, setValidationErrors] = useState<string[]>([]);
756
+ const [isButtonHovered, setIsButtonHovered] = useState(false);
757
+
758
+ const panelRef = useRef<HTMLDivElement>(null);
759
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
760
+
761
+ // Inject CSS isolation styles into head
762
+ useEffect(() => {
763
+ const styleId = `${OVERLAY_CLASS_PREFIX}-isolation-styles`;
764
+ if (!document.getElementById(styleId)) {
765
+ const styleEl = document.createElement('style');
766
+ styleEl.id = styleId;
767
+ styleEl.textContent = ISOLATION_STYLES;
768
+ document.head.appendChild(styleEl);
769
+ }
770
+ return () => {
771
+ const existing = document.getElementById(styleId);
772
+ if (existing) {
773
+ existing.remove();
774
+ }
775
+ };
776
+ }, []);
777
+
778
+ // Save comments to localStorage on change
779
+ useEffect(() => {
780
+ try {
781
+ localStorage.setItem(
782
+ STORAGE_KEY,
783
+ JSON.stringify({
784
+ comments: state.comments,
785
+ overallDirection: state.overallDirection,
786
+ })
787
+ );
788
+ } catch (err) {
789
+ console.warn('Failed to save feedback:', err);
790
+ }
791
+ }, [state.comments, state.overallDirection]);
792
+
793
+ // Focus textarea when panel opens
794
+ useEffect(() => {
795
+ if (state.panelPosition && textareaRef.current) {
796
+ textareaRef.current.focus();
797
+ }
798
+ }, [state.panelPosition]);
799
+
800
+ // Handle document clicks in feedback mode
801
+ const handleDocumentClick = useCallback(
802
+ (e: MouseEvent) => {
803
+ if (!state.isActive) return;
804
+ const target = e.target as HTMLElement;
805
+ if (isOverlayElement(target)) return;
806
+
807
+ const variantInfo = findVariantContainer(target);
808
+ if (!variantInfo) {
809
+ console.warn('Clicked element is not within a variant container');
810
+ return;
811
+ }
812
+
813
+ e.preventDefault();
814
+ e.stopPropagation();
815
+
816
+ // Calculate panel position (floating near click)
817
+ const viewportWidth = window.innerWidth;
818
+ const viewportHeight = window.innerHeight;
819
+ const panelWidth = 320;
820
+ const panelHeight = 200;
821
+
822
+ let panelX = e.clientX + 20;
823
+ let panelY = e.clientY + 20;
824
+
825
+ // Keep panel in viewport
826
+ if (panelX + panelWidth > viewportWidth - 380) { // 380 = sidebar width + margin
827
+ panelX = e.clientX - panelWidth - 20;
828
+ }
829
+ if (panelY + panelHeight > viewportHeight - 20) {
830
+ panelY = e.clientY - panelHeight - 20;
831
+ }
832
+
833
+ // Store click info on element for later use
834
+ (target as any).__feedbackClickInfo = {
835
+ variantInfo,
836
+ clickX: e.clientX,
837
+ clickY: e.clientY,
838
+ };
839
+
840
+ setState((prev) => ({
841
+ ...prev,
842
+ selectedElement: target,
843
+ panelPosition: { x: panelX, y: panelY },
844
+ currentCommentText: '',
845
+ }));
846
+ },
847
+ [state.isActive]
848
+ );
849
+
850
+ // Handle mouse move for hover highlighting
851
+ const handleDocumentMouseMove = useCallback(
852
+ (e: MouseEvent) => {
853
+ if (!state.isActive || state.panelPosition) return;
854
+ const target = e.target as HTMLElement;
855
+
856
+ if (isOverlayElement(target)) {
857
+ setHoveredElement(null);
858
+ setHoverRect(null);
859
+ return;
860
+ }
861
+
862
+ const variantInfo = findVariantContainer(target);
863
+ if (!variantInfo) {
864
+ setHoveredElement(null);
865
+ setHoverRect(null);
866
+ return;
867
+ }
868
+
869
+ setHoveredElement(target);
870
+ setHoverRect(target.getBoundingClientRect());
871
+ },
872
+ [state.isActive, state.panelPosition]
873
+ );
874
+
875
+ // Set up event listeners
876
+ useEffect(() => {
877
+ if (state.isActive) {
878
+ document.addEventListener('click', handleDocumentClick, true);
879
+ document.addEventListener('mousemove', handleDocumentMouseMove);
880
+ document.body.style.cursor = 'crosshair';
881
+ } else {
882
+ document.body.style.cursor = '';
883
+ }
884
+
885
+ return () => {
886
+ document.removeEventListener('click', handleDocumentClick, true);
887
+ document.removeEventListener('mousemove', handleDocumentMouseMove);
888
+ document.body.style.cursor = '';
889
+ };
890
+ }, [state.isActive, handleDocumentClick, handleDocumentMouseMove]);
891
+
892
+ const toggleFeedbackMode = () => {
893
+ setState((prev) => ({
894
+ ...prev,
895
+ isActive: !prev.isActive,
896
+ selectedElement: null,
897
+ panelPosition: null,
898
+ currentCommentText: '',
899
+ }));
900
+ setHoveredElement(null);
901
+ setHoverRect(null);
902
+ };
903
+
904
+ const saveComment = () => {
905
+ if (!state.selectedElement || !state.currentCommentText.trim()) return;
906
+
907
+ const clickInfo = (state.selectedElement as any).__feedbackClickInfo;
908
+ if (!clickInfo) return;
909
+
910
+ const { variantInfo, clickX, clickY } = clickInfo;
911
+ const identification = identifyElement(
912
+ state.selectedElement,
913
+ variantInfo.root,
914
+ variantInfo.variantId,
915
+ clickX,
916
+ clickY
917
+ );
918
+
919
+ const newComment: Comment = {
920
+ id: generateId(),
921
+ variant: variantInfo.variantId,
922
+ element: identification.element,
923
+ coordinates: identification.coordinates,
924
+ text: state.currentCommentText.trim(),
925
+ timestamp: Date.now(),
926
+ };
927
+
928
+ setState((prev) => ({
929
+ ...prev,
930
+ comments: [...prev.comments, newComment],
931
+ selectedElement: null,
932
+ panelPosition: null,
933
+ currentCommentText: '',
934
+ }));
935
+
936
+ delete (state.selectedElement as any).__feedbackClickInfo;
937
+ };
938
+
939
+ const cancelComment = () => {
940
+ if (state.selectedElement) {
941
+ delete (state.selectedElement as any).__feedbackClickInfo;
942
+ }
943
+ setState((prev) => ({
944
+ ...prev,
945
+ selectedElement: null,
946
+ panelPosition: null,
947
+ currentCommentText: '',
948
+ }));
949
+ };
950
+
951
+ const deleteComment = (id: string) => {
952
+ setState((prev) => ({
953
+ ...prev,
954
+ comments: prev.comments.filter((c) => c.id !== id),
955
+ }));
956
+ };
957
+
958
+ const openSubmitModal = () => {
959
+ const validation = validateFeedback(state.comments, state.overallDirection);
960
+
961
+ if (!validation.valid) {
962
+ setValidationErrors(validation.errors);
963
+ return;
964
+ }
965
+
966
+ setValidationErrors([]);
967
+ const formatted = formatAsMarkdown(state.comments, targetName, state.overallDirection);
968
+
969
+ setState((prev) => ({
970
+ ...prev,
971
+ showSubmitModal: true,
972
+ formattedOutput: formatted,
973
+ }));
974
+ };
975
+
976
+ // Primary action: Copy to clipboard only
977
+ const handleCopyToClipboard = async () => {
978
+ const success = await copyToClipboard(state.formattedOutput);
979
+ setCopySuccess(success);
980
+
981
+ if (onSubmit) {
982
+ onSubmit({
983
+ version: '1.0',
984
+ target: targetName,
985
+ timestamp: new Date().toISOString(),
986
+ comments: state.comments,
987
+ overall: state.overallDirection,
988
+ });
989
+ }
990
+
991
+ if (success) {
992
+ setTimeout(() => {
993
+ localStorage.removeItem(STORAGE_KEY);
994
+ setState((prev) => ({
995
+ ...prev,
996
+ showSubmitModal: false,
997
+ comments: [],
998
+ overallDirection: '',
999
+ isActive: false,
1000
+ }));
1001
+ setCopySuccess(false);
1002
+ }, 2000);
1003
+ }
1004
+ };
1005
+
1006
+ const commentsByVariant = state.comments.reduce(
1007
+ (acc, comment) => {
1008
+ const variant = comment.variant;
1009
+ if (!acc[variant]) acc[variant] = [];
1010
+ acc[variant].push(comment);
1011
+ return acc;
1012
+ },
1013
+ {} as Record<VariantId, Comment[]>
1014
+ );
1015
+
1016
+ const getGlobalIndex = (comment: Comment): number => {
1017
+ return state.comments.findIndex((c) => c.id === comment.id) + 1;
1018
+ };
1019
+
1020
+ if (typeof window === 'undefined') return null;
1021
+
1022
+ return createPortal(
1023
+ <>
1024
+ {/* Toggle Button - Only shown when sidebar is NOT active */}
1025
+ {!state.isActive && (
1026
+ <button
1027
+ className={`${OVERLAY_CLASS_PREFIX}-toggle`}
1028
+ style={{
1029
+ ...styles.toggleButton,
1030
+ ...(isButtonHovered ? styles.toggleButtonHover : {}),
1031
+ }}
1032
+ onClick={toggleFeedbackMode}
1033
+ onMouseEnter={() => setIsButtonHovered(true)}
1034
+ onMouseLeave={() => setIsButtonHovered(false)}
1035
+ >
1036
+ <span>💬</span>
1037
+ <span>Add Feedback</span>
1038
+ </button>
1039
+ )}
1040
+
1041
+ {/* Hover Highlight */}
1042
+ {state.isActive && hoverRect && !state.panelPosition && (
1043
+ <div
1044
+ className={`${OVERLAY_CLASS_PREFIX}-highlight`}
1045
+ style={{
1046
+ ...styles.hoverHighlight,
1047
+ left: hoverRect.left,
1048
+ top: hoverRect.top,
1049
+ width: hoverRect.width,
1050
+ height: hoverRect.height,
1051
+ }}
1052
+ />
1053
+ )}
1054
+
1055
+ {/* Comment Pins */}
1056
+ {state.comments.map((comment) => {
1057
+ const variantEl = document.querySelector(`[data-variant="${comment.variant}"]`);
1058
+ if (!variantEl) return null;
1059
+
1060
+ const variantRect = variantEl.getBoundingClientRect();
1061
+ const pinX = variantRect.left + (comment.coordinates.x / 100) * variantRect.width;
1062
+ const pinY = variantRect.top + (comment.coordinates.y / 100) * variantRect.height;
1063
+
1064
+ return (
1065
+ <div
1066
+ key={comment.id}
1067
+ className={`${OVERLAY_CLASS_PREFIX}-pin`}
1068
+ style={{ ...styles.pin, left: pinX, top: pinY }}
1069
+ title={comment.text}
1070
+ >
1071
+ {getGlobalIndex(comment)}
1072
+ </div>
1073
+ );
1074
+ })}
1075
+
1076
+ {/* Floating Comment Input Panel (appears near click) */}
1077
+ {state.panelPosition && (
1078
+ <div
1079
+ ref={panelRef}
1080
+ className={`${OVERLAY_CLASS_PREFIX}-panel`}
1081
+ style={{
1082
+ position: 'fixed',
1083
+ zIndex: 10001,
1084
+ width: '320px',
1085
+ backgroundColor: '#fff',
1086
+ borderRadius: '12px',
1087
+ boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
1088
+ border: '1px solid #e5e7eb',
1089
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
1090
+ overflow: 'hidden',
1091
+ left: state.panelPosition.x,
1092
+ top: state.panelPosition.y,
1093
+ }}
1094
+ >
1095
+ <div style={{
1096
+ padding: '12px 16px',
1097
+ borderBottom: '1px solid #e5e7eb',
1098
+ fontSize: '13px',
1099
+ fontWeight: 600,
1100
+ color: '#374151',
1101
+ backgroundColor: '#f9fafb',
1102
+ }}>
1103
+ Add Comment
1104
+ </div>
1105
+ <div style={{ padding: '16px' }}>
1106
+ <textarea
1107
+ ref={textareaRef}
1108
+ className={`${OVERLAY_CLASS_PREFIX}-textarea`}
1109
+ placeholder="What feedback do you have for this element?"
1110
+ value={state.currentCommentText}
1111
+ onChange={(e) => setState((prev) => ({ ...prev, currentCommentText: e.target.value }))}
1112
+ onKeyDown={(e) => {
1113
+ if (e.key === 'Enter' && e.metaKey) saveComment();
1114
+ if (e.key === 'Escape') cancelComment();
1115
+ }}
1116
+ />
1117
+ <div style={{ display: 'flex', gap: '8px', marginTop: '12px' }}>
1118
+ <button style={styles.buttonSecondary} onClick={cancelComment}>
1119
+ Cancel
1120
+ </button>
1121
+ <button
1122
+ style={{
1123
+ ...styles.buttonPrimary,
1124
+ opacity: state.currentCommentText.trim() ? 1 : 0.5,
1125
+ }}
1126
+ onClick={saveComment}
1127
+ disabled={!state.currentCommentText.trim()}
1128
+ >
1129
+ Save (⌘↵)
1130
+ </button>
1131
+ </div>
1132
+ </div>
1133
+ </div>
1134
+ )}
1135
+
1136
+ {/* Sidebar */}
1137
+ {state.isActive && (
1138
+ <div className={`${OVERLAY_CLASS_PREFIX}-sidebar`} style={styles.sidebar}>
1139
+ <div style={{ ...styles.sidebarHeader, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
1140
+ <div>
1141
+ <h2 style={styles.sidebarTitle}>Design Feedback</h2>
1142
+ <div style={styles.sidebarSubtitle}>
1143
+ {state.comments.length} comment{state.comments.length !== 1 ? 's' : ''} on {targetName}
1144
+ </div>
1145
+ </div>
1146
+ <button
1147
+ className={`${OVERLAY_CLASS_PREFIX}-close`}
1148
+ style={{
1149
+ padding: '6px 10px',
1150
+ fontSize: '13px',
1151
+ fontWeight: 500,
1152
+ color: '#ef4444',
1153
+ backgroundColor: '#fef2f2',
1154
+ border: '1px solid #fecaca',
1155
+ borderRadius: '6px',
1156
+ cursor: 'pointer',
1157
+ transition: 'all 150ms ease',
1158
+ flexShrink: 0,
1159
+ }}
1160
+ onClick={toggleFeedbackMode}
1161
+ >
1162
+ ✕ Exit
1163
+ </button>
1164
+ </div>
1165
+
1166
+ <div style={styles.sidebarContent}>
1167
+ {variants.map((variantId) => {
1168
+ const variantComments = commentsByVariant[variantId as VariantId];
1169
+ if (!variantComments || variantComments.length === 0) return null;
1170
+
1171
+ return (
1172
+ <div key={variantId} style={styles.sidebarSection}>
1173
+ <div style={styles.sidebarSectionTitle}>Variant {variantId}</div>
1174
+ {variantComments.map((comment) => (
1175
+ <div key={comment.id} style={styles.commentCard}>
1176
+ <div style={styles.commentCardHeader}>
1177
+ <div style={styles.commentCardPin}>{getGlobalIndex(comment)}</div>
1178
+ <div style={styles.commentCardElement}>
1179
+ {comment.element.readablePath.split(' > ').slice(-1)[0]}
1180
+ </div>
1181
+ <button style={styles.commentCardDelete} onClick={() => deleteComment(comment.id)}>
1182
+ Delete
1183
+ </button>
1184
+ </div>
1185
+ <div style={styles.commentCardText}>{comment.text}</div>
1186
+ </div>
1187
+ ))}
1188
+ </div>
1189
+ );
1190
+ })}
1191
+
1192
+ {state.comments.length === 0 && (
1193
+ <div style={{ color: '#6b7280', textAlign: 'center', padding: '40px 20px' }}>
1194
+ <p style={{ fontSize: '14px', marginBottom: '8px' }}>Click on any element to add feedback</p>
1195
+ <p style={{ fontSize: '13px', opacity: 0.8 }}>Comments will appear here</p>
1196
+ </div>
1197
+ )}
1198
+ </div>
1199
+
1200
+ {/* Overall Direction */}
1201
+ <div style={styles.overallSection}>
1202
+ <div style={styles.overallLabel}>Overall Direction</div>
1203
+ <textarea
1204
+ className={`${OVERLAY_CLASS_PREFIX}-overall-textarea`}
1205
+ placeholder="What's the overall direction? e.g., 'Go with Variant B's layout but use A's button styling...'"
1206
+ value={state.overallDirection}
1207
+ onChange={(e) => setState((prev) => ({ ...prev, overallDirection: e.target.value }))}
1208
+ />
1209
+ </div>
1210
+
1211
+ {/* Validation Errors */}
1212
+ {validationErrors.length > 0 && (
1213
+ <div style={{ padding: '0 16px' }}>
1214
+ <div style={styles.errorMessage}>
1215
+ {validationErrors.map((err, i) => (<div key={i}>{err}</div>))}
1216
+ </div>
1217
+ </div>
1218
+ )}
1219
+
1220
+ {/* Submit Section */}
1221
+ <div style={styles.submitSection}>
1222
+ <button
1223
+ style={{
1224
+ ...styles.submitButton,
1225
+ ...(state.comments.length === 0 ? styles.submitButtonDisabled : {}),
1226
+ }}
1227
+ onClick={openSubmitModal}
1228
+ disabled={state.comments.length === 0}
1229
+ >
1230
+ Submit Feedback
1231
+ </button>
1232
+ </div>
1233
+ </div>
1234
+ )}
1235
+
1236
+ {/* Submit Modal */}
1237
+ {state.showSubmitModal && (
1238
+ <div
1239
+ className={`${OVERLAY_CLASS_PREFIX}-modal`}
1240
+ style={styles.modalOverlay}
1241
+ onClick={(e) => {
1242
+ if (e.target === e.currentTarget) {
1243
+ setState((prev) => ({ ...prev, showSubmitModal: false }));
1244
+ setCopySuccess(false);
1245
+ }
1246
+ }}
1247
+ >
1248
+ <div style={styles.modal}>
1249
+ <div style={styles.modalHeader}>
1250
+ <h3 style={styles.modalTitle}>Copy Feedback</h3>
1251
+ <button
1252
+ style={styles.modalClose}
1253
+ onClick={() => {
1254
+ setState((prev) => ({ ...prev, showSubmitModal: false }));
1255
+ setCopySuccess(false);
1256
+ }}
1257
+ >
1258
+
1259
+ </button>
1260
+ </div>
1261
+
1262
+ <div style={styles.modalBody}>
1263
+ {copySuccess && (
1264
+ <div style={styles.successMessage}>
1265
+ <span>✓</span>
1266
+ <span>Copied! Paste this into your terminal to send to Claude.</span>
1267
+ </div>
1268
+ )}
1269
+
1270
+ <div style={{ marginBottom: '12px', fontSize: '14px', color: '#4b5563' }}>
1271
+ Click &quot;Copy to Clipboard&quot; then paste into your terminal to share feedback with Claude.
1272
+ </div>
1273
+
1274
+ <div style={styles.modalPreview}>{state.formattedOutput}</div>
1275
+ </div>
1276
+
1277
+ <div style={styles.modalFooter}>
1278
+ <button
1279
+ style={styles.buttonSecondary}
1280
+ onClick={() => {
1281
+ setState((prev) => ({ ...prev, showSubmitModal: false }));
1282
+ setCopySuccess(false);
1283
+ }}
1284
+ >
1285
+ Cancel
1286
+ </button>
1287
+ <button style={styles.submitButton} onClick={handleCopyToClipboard}>
1288
+ {copySuccess ? 'Copied!' : 'Copy to Clipboard'}
1289
+ </button>
1290
+ </div>
1291
+ </div>
1292
+ </div>
1293
+ )}
1294
+ </>,
1295
+ document.body
1296
+ );
1297
+ }
1298
+
1299
+ export default FeedbackOverlay;