liteagents 2.5.3 → 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 +31 -0
- package/README.md +51 -45
- package/installer/cli.js +1 -1
- package/installer/installation-engine.js +82 -21
- package/installer/package-manager.js +34 -3
- package/package.json +3 -2
- 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 +15 -12
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design Lab Interactive Feedback System - Element Selector Utilities
|
|
3
|
+
*
|
|
4
|
+
* Generates reliable CSS selectors and human-readable paths for clicked elements.
|
|
5
|
+
* Uses multiple fallback strategies to ensure Claude can locate elements in code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ElementIdentifier, VariantId, Coordinates } from './types';
|
|
9
|
+
import { OVERLAY_CLASS_PREFIX } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Regex pattern to detect CSS-in-JS generated class names.
|
|
13
|
+
* Matches patterns like: css-1a2b3c, sc-abc123, emotion-xyz, etc.
|
|
14
|
+
*/
|
|
15
|
+
const CSS_IN_JS_PATTERN = /^(css|sc|emotion|styled|chakra|mui|ant)-[a-z0-9]+$/i;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Regex pattern to detect hash-like suffixes in class names.
|
|
19
|
+
* Matches: component_abc123, button__hash, etc.
|
|
20
|
+
*/
|
|
21
|
+
const HASH_SUFFIX_PATTERN = /_[a-z0-9]{5,}$/i;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if a class name appears to be generated by CSS-in-JS.
|
|
25
|
+
*/
|
|
26
|
+
function isCssInJsClass(className: string): boolean {
|
|
27
|
+
return (
|
|
28
|
+
CSS_IN_JS_PATTERN.test(className) ||
|
|
29
|
+
HASH_SUFFIX_PATTERN.test(className) ||
|
|
30
|
+
// Tailwind-like utility classes are fine, but random hashes aren't
|
|
31
|
+
/^[a-z]{1,3}[0-9]{4,}$/i.test(className)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Filter class names to remove CSS-in-JS generated ones.
|
|
37
|
+
* Returns only meaningful, human-authored class names.
|
|
38
|
+
*/
|
|
39
|
+
function filterClasses(classNames: string): string {
|
|
40
|
+
if (!classNames) return '';
|
|
41
|
+
|
|
42
|
+
return classNames
|
|
43
|
+
.split(/\s+/)
|
|
44
|
+
.filter((cls) => cls && !isCssInJsClass(cls))
|
|
45
|
+
.join(' ');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get relevant attributes from an element.
|
|
50
|
+
* Focuses on data-*, aria-*, and id attributes.
|
|
51
|
+
*/
|
|
52
|
+
function getRelevantAttributes(element: HTMLElement): Record<string, string> {
|
|
53
|
+
const attrs: Record<string, string> = {};
|
|
54
|
+
const relevantPrefixes = ['data-', 'aria-'];
|
|
55
|
+
|
|
56
|
+
for (const attr of Array.from(element.attributes)) {
|
|
57
|
+
if (
|
|
58
|
+
relevantPrefixes.some((prefix) => attr.name.startsWith(prefix)) ||
|
|
59
|
+
attr.name === 'id' ||
|
|
60
|
+
attr.name === 'role' ||
|
|
61
|
+
attr.name === 'type' ||
|
|
62
|
+
attr.name === 'name'
|
|
63
|
+
) {
|
|
64
|
+
attrs[attr.name] = attr.value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return attrs;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate a CSS selector for an element, prioritizing reliable identifiers.
|
|
73
|
+
*
|
|
74
|
+
* Priority order:
|
|
75
|
+
* 1. data-testid or data-cy (most reliable for code lookup)
|
|
76
|
+
* 2. Unique ID
|
|
77
|
+
* 3. Semantic + accessible name (aria-label, role)
|
|
78
|
+
* 4. Meaningful class path
|
|
79
|
+
* 5. Tag + text content hint
|
|
80
|
+
* 6. Structural path (fallback)
|
|
81
|
+
*/
|
|
82
|
+
export function generateSelector(
|
|
83
|
+
element: HTMLElement,
|
|
84
|
+
variantRoot: HTMLElement
|
|
85
|
+
): string {
|
|
86
|
+
// Priority 1: data-testid or data-cy
|
|
87
|
+
const testId =
|
|
88
|
+
element.getAttribute('data-testid') || element.getAttribute('data-cy');
|
|
89
|
+
if (testId) {
|
|
90
|
+
return `[data-testid="${testId}"]`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Priority 2: Unique ID (not auto-generated)
|
|
94
|
+
const id = element.id;
|
|
95
|
+
if (id && !isCssInJsClass(id) && !id.startsWith(':')) {
|
|
96
|
+
return `#${id}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Priority 3: Semantic + aria-label
|
|
100
|
+
const ariaLabel = element.getAttribute('aria-label');
|
|
101
|
+
const role = element.getAttribute('role');
|
|
102
|
+
if (ariaLabel) {
|
|
103
|
+
const tag = element.tagName.toLowerCase();
|
|
104
|
+
return `${tag}[aria-label="${ariaLabel}"]`;
|
|
105
|
+
}
|
|
106
|
+
if (role && !['generic', 'presentation', 'none'].includes(role)) {
|
|
107
|
+
const tag = element.tagName.toLowerCase();
|
|
108
|
+
// Add context to make more unique
|
|
109
|
+
const name = element.getAttribute('name');
|
|
110
|
+
if (name) {
|
|
111
|
+
return `${tag}[role="${role}"][name="${name}"]`;
|
|
112
|
+
}
|
|
113
|
+
return `${tag}[role="${role}"]`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Priority 4: Meaningful class path
|
|
117
|
+
const meaningfulClasses = filterClasses(element.className);
|
|
118
|
+
if (meaningfulClasses) {
|
|
119
|
+
const classSelector = meaningfulClasses
|
|
120
|
+
.split(/\s+/)
|
|
121
|
+
.slice(0, 2) // Use first 2 meaningful classes
|
|
122
|
+
.map((c) => `.${c}`)
|
|
123
|
+
.join('');
|
|
124
|
+
|
|
125
|
+
// Check if this is unique enough within the variant
|
|
126
|
+
const matches = variantRoot.querySelectorAll(classSelector);
|
|
127
|
+
if (matches.length === 1) {
|
|
128
|
+
return classSelector;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Add parent context
|
|
132
|
+
const parent = element.parentElement;
|
|
133
|
+
if (parent && parent !== variantRoot) {
|
|
134
|
+
const parentClasses = filterClasses(parent.className);
|
|
135
|
+
if (parentClasses) {
|
|
136
|
+
const parentSelector = parentClasses
|
|
137
|
+
.split(/\s+/)
|
|
138
|
+
.slice(0, 1)
|
|
139
|
+
.map((c) => `.${c}`)
|
|
140
|
+
.join('');
|
|
141
|
+
return `${parentSelector} > ${classSelector}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return classSelector;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Priority 5: Tag + type for inputs/buttons
|
|
149
|
+
const tag = element.tagName.toLowerCase();
|
|
150
|
+
const type = element.getAttribute('type');
|
|
151
|
+
const name = element.getAttribute('name');
|
|
152
|
+
|
|
153
|
+
if (tag === 'input' || tag === 'button' || tag === 'select') {
|
|
154
|
+
if (name) {
|
|
155
|
+
return `${tag}[name="${name}"]`;
|
|
156
|
+
}
|
|
157
|
+
if (type) {
|
|
158
|
+
return `${tag}[type="${type}"]`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Priority 6: Structural path (fallback)
|
|
163
|
+
return generateStructuralPath(element, variantRoot);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Generate a structural path selector (nth-child based).
|
|
168
|
+
* Used as a fallback when no better identifiers exist.
|
|
169
|
+
*/
|
|
170
|
+
function generateStructuralPath(
|
|
171
|
+
element: HTMLElement,
|
|
172
|
+
variantRoot: HTMLElement
|
|
173
|
+
): string {
|
|
174
|
+
const path: string[] = [];
|
|
175
|
+
let current: HTMLElement | null = element;
|
|
176
|
+
|
|
177
|
+
while (current && current !== variantRoot && current.parentElement) {
|
|
178
|
+
const parent = current.parentElement;
|
|
179
|
+
const siblings = Array.from(parent.children).filter(
|
|
180
|
+
(child) => child.tagName === current!.tagName
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const tag = current.tagName.toLowerCase();
|
|
184
|
+
|
|
185
|
+
if (siblings.length === 1) {
|
|
186
|
+
path.unshift(tag);
|
|
187
|
+
} else {
|
|
188
|
+
const index = siblings.indexOf(current) + 1;
|
|
189
|
+
path.unshift(`${tag}:nth-child(${index})`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
current = parent;
|
|
193
|
+
|
|
194
|
+
// Limit depth to avoid overly long selectors
|
|
195
|
+
if (path.length >= 4) break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return path.join(' > ');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Generate a human-readable path for an element.
|
|
203
|
+
* Example: "Variant B > Form > Submit Button"
|
|
204
|
+
*/
|
|
205
|
+
export function generateReadablePath(
|
|
206
|
+
element: HTMLElement,
|
|
207
|
+
variantRoot: HTMLElement,
|
|
208
|
+
variantId: VariantId
|
|
209
|
+
): string {
|
|
210
|
+
const parts: string[] = [`Variant ${variantId}`];
|
|
211
|
+
const path: HTMLElement[] = [];
|
|
212
|
+
|
|
213
|
+
let current: HTMLElement | null = element;
|
|
214
|
+
while (current && current !== variantRoot) {
|
|
215
|
+
path.unshift(current);
|
|
216
|
+
current = current.parentElement;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Take up to 3 ancestors for context
|
|
220
|
+
const relevantPath = path.slice(-3);
|
|
221
|
+
|
|
222
|
+
for (const el of relevantPath) {
|
|
223
|
+
const label = getElementLabel(el);
|
|
224
|
+
if (label) {
|
|
225
|
+
parts.push(label);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return parts.join(' > ');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get a human-readable label for an element.
|
|
234
|
+
*/
|
|
235
|
+
function getElementLabel(element: HTMLElement): string {
|
|
236
|
+
const tag = element.tagName.toLowerCase();
|
|
237
|
+
|
|
238
|
+
// Use aria-label if available
|
|
239
|
+
const ariaLabel = element.getAttribute('aria-label');
|
|
240
|
+
if (ariaLabel) {
|
|
241
|
+
return ariaLabel;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Use text content for buttons and links
|
|
245
|
+
if (tag === 'button' || tag === 'a') {
|
|
246
|
+
const text = element.textContent?.trim().slice(0, 30);
|
|
247
|
+
if (text) {
|
|
248
|
+
return `${capitalizeTag(tag)}: "${text}"`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Use name or placeholder for inputs
|
|
253
|
+
if (tag === 'input' || tag === 'select' || tag === 'textarea') {
|
|
254
|
+
const name =
|
|
255
|
+
element.getAttribute('name') ||
|
|
256
|
+
element.getAttribute('placeholder') ||
|
|
257
|
+
element.getAttribute('aria-label');
|
|
258
|
+
if (name) {
|
|
259
|
+
return `${capitalizeTag(tag)}: ${name}`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Use heading level
|
|
264
|
+
if (/^h[1-6]$/.test(tag)) {
|
|
265
|
+
const text = element.textContent?.trim().slice(0, 30);
|
|
266
|
+
if (text) {
|
|
267
|
+
return `Heading: "${text}"`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Use meaningful classes
|
|
272
|
+
const classes = filterClasses(element.className);
|
|
273
|
+
if (classes) {
|
|
274
|
+
const primary = classes.split(/\s+/)[0];
|
|
275
|
+
return formatClassName(primary);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Fallback to tag name
|
|
279
|
+
return capitalizeTag(tag);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Capitalize a tag name for display.
|
|
284
|
+
*/
|
|
285
|
+
function capitalizeTag(tag: string): string {
|
|
286
|
+
const tagLabels: Record<string, string> = {
|
|
287
|
+
div: 'Container',
|
|
288
|
+
section: 'Section',
|
|
289
|
+
article: 'Article',
|
|
290
|
+
nav: 'Navigation',
|
|
291
|
+
header: 'Header',
|
|
292
|
+
footer: 'Footer',
|
|
293
|
+
main: 'Main',
|
|
294
|
+
aside: 'Sidebar',
|
|
295
|
+
form: 'Form',
|
|
296
|
+
ul: 'List',
|
|
297
|
+
ol: 'Numbered List',
|
|
298
|
+
li: 'List Item',
|
|
299
|
+
button: 'Button',
|
|
300
|
+
a: 'Link',
|
|
301
|
+
input: 'Input',
|
|
302
|
+
select: 'Dropdown',
|
|
303
|
+
textarea: 'Text Area',
|
|
304
|
+
img: 'Image',
|
|
305
|
+
span: 'Text',
|
|
306
|
+
p: 'Paragraph',
|
|
307
|
+
table: 'Table',
|
|
308
|
+
tr: 'Row',
|
|
309
|
+
td: 'Cell',
|
|
310
|
+
th: 'Header Cell',
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
return tagLabels[tag] || tag.charAt(0).toUpperCase() + tag.slice(1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Format a class name for display.
|
|
318
|
+
* Converts kebab-case or camelCase to readable text.
|
|
319
|
+
*/
|
|
320
|
+
function formatClassName(className: string): string {
|
|
321
|
+
return className
|
|
322
|
+
.replace(/[-_]/g, ' ')
|
|
323
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
324
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get truncated text content from an element.
|
|
329
|
+
*/
|
|
330
|
+
export function getTextContent(element: HTMLElement, maxLength = 50): string {
|
|
331
|
+
const text = element.textContent?.trim() || '';
|
|
332
|
+
if (text.length <= maxLength) {
|
|
333
|
+
return text;
|
|
334
|
+
}
|
|
335
|
+
return text.slice(0, maxLength - 3) + '...';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Calculate coordinates as percentages within the variant container.
|
|
340
|
+
*/
|
|
341
|
+
export function calculateCoordinates(
|
|
342
|
+
element: HTMLElement,
|
|
343
|
+
variantRoot: HTMLElement,
|
|
344
|
+
clickX: number,
|
|
345
|
+
clickY: number
|
|
346
|
+
): Coordinates {
|
|
347
|
+
const rootRect = variantRoot.getBoundingClientRect();
|
|
348
|
+
|
|
349
|
+
const x = ((clickX - rootRect.left) / rootRect.width) * 100;
|
|
350
|
+
const y = ((clickY - rootRect.top) / rootRect.height) * 100;
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
x: Math.round(x * 10) / 10,
|
|
354
|
+
y: Math.round(y * 10) / 10,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Identify an element completely, returning all identification data.
|
|
360
|
+
*/
|
|
361
|
+
export function identifyElement(
|
|
362
|
+
element: HTMLElement,
|
|
363
|
+
variantRoot: HTMLElement,
|
|
364
|
+
variantId: VariantId,
|
|
365
|
+
clickX: number,
|
|
366
|
+
clickY: number
|
|
367
|
+
): { element: ElementIdentifier; coordinates: Coordinates } {
|
|
368
|
+
return {
|
|
369
|
+
element: {
|
|
370
|
+
selector: generateSelector(element, variantRoot),
|
|
371
|
+
readablePath: generateReadablePath(element, variantRoot, variantId),
|
|
372
|
+
tagName: element.tagName.toLowerCase(),
|
|
373
|
+
textContent: getTextContent(element),
|
|
374
|
+
className: filterClasses(element.className),
|
|
375
|
+
attributes: getRelevantAttributes(element),
|
|
376
|
+
},
|
|
377
|
+
coordinates: calculateCoordinates(element, variantRoot, clickX, clickY),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Check if an element is part of the feedback overlay itself.
|
|
383
|
+
* Used to prevent selecting overlay elements.
|
|
384
|
+
*/
|
|
385
|
+
export function isOverlayElement(element: HTMLElement): boolean {
|
|
386
|
+
let current: HTMLElement | null = element;
|
|
387
|
+
|
|
388
|
+
while (current) {
|
|
389
|
+
if (
|
|
390
|
+
current.className &&
|
|
391
|
+
typeof current.className === 'string' &&
|
|
392
|
+
current.className.includes(OVERLAY_CLASS_PREFIX)
|
|
393
|
+
) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
current = current.parentElement;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Find the variant container that an element belongs to.
|
|
404
|
+
* Looks for elements with data-variant attribute.
|
|
405
|
+
*/
|
|
406
|
+
export function findVariantContainer(
|
|
407
|
+
element: HTMLElement
|
|
408
|
+
): { root: HTMLElement; variantId: VariantId } | null {
|
|
409
|
+
let current: HTMLElement | null = element;
|
|
410
|
+
|
|
411
|
+
while (current) {
|
|
412
|
+
const variantId = current.getAttribute('data-variant');
|
|
413
|
+
if (variantId && /^[A-F]$/.test(variantId)) {
|
|
414
|
+
return {
|
|
415
|
+
root: current,
|
|
416
|
+
variantId: variantId as VariantId,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
current = current.parentElement;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design Lab Interactive Feedback System - Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* These types define the structure for capturing, storing, and
|
|
5
|
+
* formatting user feedback on design variants.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Supported variant identifiers */
|
|
9
|
+
export type VariantId = 'A' | 'B' | 'C' | 'D' | 'E' | 'F';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Element identification with multiple fallback strategies.
|
|
13
|
+
* Claude uses these identifiers to locate elements in code.
|
|
14
|
+
*/
|
|
15
|
+
export interface ElementIdentifier {
|
|
16
|
+
/** Primary: CSS selector (data-testid preferred) */
|
|
17
|
+
selector: string;
|
|
18
|
+
/** Secondary: Human-readable path for context */
|
|
19
|
+
readablePath: string;
|
|
20
|
+
/** HTML tag name */
|
|
21
|
+
tagName: string;
|
|
22
|
+
/** First 50 chars of text content (for disambiguation) */
|
|
23
|
+
textContent: string;
|
|
24
|
+
/** Original class names (CSS-in-JS hashes filtered out) */
|
|
25
|
+
className: string;
|
|
26
|
+
/** Relevant attributes (data-*, aria-*, id) */
|
|
27
|
+
attributes: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Position coordinates as percentages within the variant container.
|
|
32
|
+
* Used as visual backup when selectors fail.
|
|
33
|
+
*/
|
|
34
|
+
export interface Coordinates {
|
|
35
|
+
/** Percentage from left edge of variant container */
|
|
36
|
+
x: number;
|
|
37
|
+
/** Percentage from top edge of variant container */
|
|
38
|
+
y: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A single feedback comment attached to an element.
|
|
43
|
+
*/
|
|
44
|
+
export interface Comment {
|
|
45
|
+
/** Unique identifier for this comment */
|
|
46
|
+
id: string;
|
|
47
|
+
/** Which variant this comment belongs to */
|
|
48
|
+
variant: VariantId;
|
|
49
|
+
/** Element identification data */
|
|
50
|
+
element: ElementIdentifier;
|
|
51
|
+
/** Click position within the variant */
|
|
52
|
+
coordinates: Coordinates;
|
|
53
|
+
/** User's feedback text */
|
|
54
|
+
text: string;
|
|
55
|
+
/** Unix timestamp when created */
|
|
56
|
+
timestamp: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Complete feedback payload ready for clipboard/download.
|
|
61
|
+
*/
|
|
62
|
+
export interface FeedbackPayload {
|
|
63
|
+
/** Schema version for future compatibility */
|
|
64
|
+
version: '1.0';
|
|
65
|
+
/** Target component or page name */
|
|
66
|
+
target: string;
|
|
67
|
+
/** ISO timestamp of submission */
|
|
68
|
+
timestamp: string;
|
|
69
|
+
/** All comments grouped by creation order */
|
|
70
|
+
comments: Comment[];
|
|
71
|
+
/** User's overall direction/synthesis guidance */
|
|
72
|
+
overall: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* FeedbackOverlay component props.
|
|
77
|
+
*/
|
|
78
|
+
export interface FeedbackOverlayProps {
|
|
79
|
+
/** Name of the component/page being designed */
|
|
80
|
+
targetName: string;
|
|
81
|
+
/** Optional: variant IDs to display (defaults to A-E) */
|
|
82
|
+
variants?: VariantId[];
|
|
83
|
+
/** Optional: callback when feedback is submitted */
|
|
84
|
+
onSubmit?: (payload: FeedbackPayload) => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Internal state for the feedback overlay.
|
|
89
|
+
*/
|
|
90
|
+
export interface FeedbackState {
|
|
91
|
+
/** Whether feedback mode is active */
|
|
92
|
+
isActive: boolean;
|
|
93
|
+
/** Currently selected element (if any) */
|
|
94
|
+
selectedElement: HTMLElement | null;
|
|
95
|
+
/** Position for the comment input panel */
|
|
96
|
+
panelPosition: { x: number; y: number } | null;
|
|
97
|
+
/** All comments created in this session */
|
|
98
|
+
comments: Comment[];
|
|
99
|
+
/** Text in the current comment input */
|
|
100
|
+
currentCommentText: string;
|
|
101
|
+
/** Overall direction text */
|
|
102
|
+
overallDirection: string;
|
|
103
|
+
/** Whether the submit modal is showing */
|
|
104
|
+
showSubmitModal: boolean;
|
|
105
|
+
/** The formatted output (for modal preview) */
|
|
106
|
+
formattedOutput: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Storage key for persisting comments in localStorage.
|
|
111
|
+
*/
|
|
112
|
+
export const STORAGE_KEY = 'design-lab-feedback';
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* CSS class prefix for feedback overlay elements.
|
|
116
|
+
* Used to identify and exclude overlay elements from selection.
|
|
117
|
+
*/
|
|
118
|
+
export const OVERLAY_CLASS_PREFIX = 'dl-feedback';
|