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.
- package/CHANGELOG.md +76 -15
- package/README.md +58 -30
- package/docs/INSTALLATION_DEMO.md +1 -1
- package/docs/INSTALLATION_LOCATIONS.md +5 -5
- package/docs/INSTALLER_GUIDE.md +5 -5
- package/docs/KNOWLEDGE_BASE.md +5 -5
- package/docs/MIGRATION.md +1 -1
- package/docs/PASS_QUICK_START.md +1 -1
- package/docs/PRIVACY.md +1 -1
- package/docs/PUBLISHING.md +7 -7
- package/docs/RELEASE_NOTES_1.2.0.md +2 -2
- package/docs/SILENT_MODE_GUIDE.md +1 -1
- package/installer/cli.js +1 -1
- package/installer/installation-engine.js +82 -21
- package/installer/package-manager.js +34 -3
- package/package.json +7 -6
- package/packages/ampcode/AGENT.md +1 -0
- package/packages/ampcode/commands/friction/friction.js +19 -4
- package/packages/ampcode/commands/live-canvas/DESIGN_PRINCIPLES.md +2140 -0
- package/packages/ampcode/commands/live-canvas/README.md +273 -0
- package/packages/ampcode/commands/live-canvas/templates/.claude/settings.local.json +8 -0
- package/packages/ampcode/commands/live-canvas/templates/demo/post-variants.html +205 -0
- package/packages/ampcode/commands/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +1299 -0
- package/packages/ampcode/commands/live-canvas/templates/feedback-react/format-utils.ts +283 -0
- package/packages/ampcode/commands/live-canvas/templates/feedback-react/index.ts +62 -0
- package/packages/ampcode/commands/live-canvas/templates/feedback-react/selector-utils.ts +423 -0
- package/packages/ampcode/commands/live-canvas/templates/feedback-react/types.ts +118 -0
- package/packages/ampcode/commands/live-canvas/templates/overlay-vanilla.js +477 -0
- package/packages/ampcode/commands/live-canvas.md +1104 -0
- package/packages/claude/CLAUDE.md +1 -0
- package/packages/claude/commands/friction/friction.js +19 -4
- package/packages/claude/plugins/live-canvas-marketplace/.claude-plugin/marketplace.json +14 -0
- package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/.claude-plugin/plugin.json +18 -0
- package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/README.md +89 -0
- package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/package-lock.json +1142 -0
- package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/package.json +17 -0
- package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/schema.json +37 -0
- package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/server.js +179 -0
- package/packages/claude/plugins/live-canvas-marketplace/setup.sh +61 -0
- package/packages/claude/skills/live-canvas/DESIGN_PRINCIPLES.md +2140 -0
- package/packages/claude/skills/live-canvas/INTEGRATION_NOTES.md +210 -0
- package/packages/claude/skills/live-canvas/README.md +273 -0
- package/packages/claude/skills/live-canvas/SKILL.md +1119 -0
- package/packages/claude/skills/live-canvas/templates/.claude/settings.local.json +8 -0
- package/packages/claude/skills/live-canvas/templates/demo/post-variants.html +205 -0
- package/packages/claude/skills/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +1299 -0
- package/packages/claude/skills/live-canvas/templates/feedback-react/format-utils.ts +283 -0
- package/packages/claude/skills/live-canvas/templates/feedback-react/index.ts +62 -0
- package/packages/claude/skills/live-canvas/templates/feedback-react/selector-utils.ts +423 -0
- package/packages/claude/skills/live-canvas/templates/feedback-react/types.ts +118 -0
- package/packages/claude/skills/live-canvas/templates/overlay-vanilla.js +477 -0
- package/packages/claude/variants.json +2 -1
- package/packages/droid/AGENTS.md +1 -0
- package/packages/droid/commands/friction/friction.js +19 -4
- package/packages/droid/commands/live-canvas/DESIGN_PRINCIPLES.md +2140 -0
- package/packages/droid/commands/live-canvas/README.md +273 -0
- package/packages/droid/commands/live-canvas/templates/.claude/settings.local.json +8 -0
- package/packages/droid/commands/live-canvas/templates/demo/post-variants.html +205 -0
- package/packages/droid/commands/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +1299 -0
- package/packages/droid/commands/live-canvas/templates/feedback-react/format-utils.ts +283 -0
- package/packages/droid/commands/live-canvas/templates/feedback-react/index.ts +62 -0
- package/packages/droid/commands/live-canvas/templates/feedback-react/selector-utils.ts +423 -0
- package/packages/droid/commands/live-canvas/templates/feedback-react/types.ts +118 -0
- package/packages/droid/commands/live-canvas/templates/overlay-vanilla.js +477 -0
- package/packages/droid/commands/live-canvas.md +1104 -0
- package/packages/opencode/AGENTS.md +1 -0
- package/packages/opencode/command/friction/friction.js +19 -4
- package/packages/opencode/command/live-canvas/DESIGN_PRINCIPLES.md +2140 -0
- package/packages/opencode/command/live-canvas/README.md +273 -0
- package/packages/opencode/command/live-canvas/templates/.claude/settings.local.json +8 -0
- package/packages/opencode/command/live-canvas/templates/demo/post-variants.html +205 -0
- package/packages/opencode/command/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +1299 -0
- package/packages/opencode/command/live-canvas/templates/feedback-react/format-utils.ts +283 -0
- package/packages/opencode/command/live-canvas/templates/feedback-react/index.ts +62 -0
- package/packages/opencode/command/live-canvas/templates/feedback-react/selector-utils.ts +423 -0
- package/packages/opencode/command/live-canvas/templates/feedback-react/types.ts +118 -0
- package/packages/opencode/command/live-canvas/templates/overlay-vanilla.js +477 -0
- package/packages/opencode/command/live-canvas.md +1104 -0
- package/packages/opencode/opencode.jsonc +4 -0
- 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 "Copy to Clipboard" 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;
|