chrometools-mcp 2.4.0 → 2.5.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 +139 -0
- package/README.md +102 -5
- package/RELEASE_NOTES_v2.5.0.md +109 -0
- package/VIDEO_SCRIPTS.md +2116 -0
- package/element-finder-utils.js +138 -28
- package/figma-tools.js +120 -0
- package/index.js +430 -9
- package/npm_publish_output.txt +0 -0
- package/package.json +1 -1
- package/server/tool-definitions.js +63 -5
- package/server/tool-groups.js +4 -5
- package/server/tool-schemas.js +31 -0
- package/tools/tool-schemas.js +1 -0
package/element-finder-utils.js
CHANGED
|
@@ -305,73 +305,181 @@ function isSafeSelectorValue(value) {
|
|
|
305
305
|
return !/["'\\[\]{}()]/.test(value);
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Check if a class name is a Tailwind CSS utility class
|
|
310
|
+
* Tailwind classes contain special characters that break CSS selectors:
|
|
311
|
+
* - Colons (:) for variants like hover:, focus:, md:, etc.
|
|
312
|
+
* - Slashes (/) for fractions like w-1/2
|
|
313
|
+
* - Brackets ([]) for arbitrary values like bg-[#1da1f2]
|
|
314
|
+
* - Dots (.) in decimal values
|
|
315
|
+
*/
|
|
316
|
+
function isTailwindClass(className) {
|
|
317
|
+
if (!className || typeof className !== 'string') return false;
|
|
318
|
+
|
|
319
|
+
// Check for special characters that indicate Tailwind variants/utilities
|
|
320
|
+
if (/[:\/\[\]]/.test(className)) {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Common Tailwind utility prefixes
|
|
325
|
+
const tailwindPrefixes = [
|
|
326
|
+
'bg-', 'text-', 'border-', 'rounded-', 'shadow-', 'font-', 'leading-',
|
|
327
|
+
'flex-', 'grid-', 'p-', 'px-', 'py-', 'pt-', 'pb-', 'pl-', 'pr-',
|
|
328
|
+
'm-', 'mx-', 'my-', 'mt-', 'mb-', 'ml-', 'mr-', 'space-',
|
|
329
|
+
'w-', 'h-', 'min-', 'max-', 'gap-', 'inset-', 'top-', 'right-', 'bottom-', 'left-',
|
|
330
|
+
'justify-', 'items-', 'content-', 'self-', 'place-',
|
|
331
|
+
'overflow-', 'opacity-', 'cursor-', 'transition-', 'transform-',
|
|
332
|
+
'duration-', 'ease-', 'delay-', 'animate-', 'scale-', 'rotate-', 'translate-',
|
|
333
|
+
'z-', 'order-', 'col-', 'row-', 'auto-', 'tracking-', 'select-',
|
|
334
|
+
'sr-', 'not-', 'pointer-', 'resize-', 'list-', 'appearance-',
|
|
335
|
+
'underline', 'line-through', 'no-underline', 'uppercase', 'lowercase', 'capitalize',
|
|
336
|
+
'italic', 'not-italic', 'ordinal', 'slashed-zero', 'lining-nums', 'oldstyle-nums',
|
|
337
|
+
'proportional-nums', 'tabular-nums', 'diagonal-fractions', 'stacked-fractions',
|
|
338
|
+
'hidden', 'block', 'inline', 'flex', 'grid', 'table', 'contents',
|
|
339
|
+
'absolute', 'relative', 'fixed', 'sticky', 'static'
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
// Check if className starts with known Tailwind prefix
|
|
343
|
+
return tailwindPrefixes.some(prefix => className.startsWith(prefix));
|
|
344
|
+
}
|
|
345
|
+
|
|
308
346
|
/**
|
|
309
347
|
* Generate unique CSS selector for an element
|
|
348
|
+
* Priority: id → data-testid → data-* → aria-label → role → semantic classes → nth-child
|
|
349
|
+
* Filters out Tailwind CSS classes to avoid invalid selectors
|
|
310
350
|
*/
|
|
311
351
|
function getUniqueSelectorInPage(element) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
352
|
+
const tagName = element.tagName.toLowerCase();
|
|
353
|
+
|
|
354
|
+
// 1. Try ID first (highest priority)
|
|
355
|
+
if (element.id && isSafeSelectorValue(element.id)) {
|
|
356
|
+
try {
|
|
357
|
+
return `#${CSS.escape(element.id)}`;
|
|
358
|
+
} catch (e) {
|
|
359
|
+
// CSS.escape not available in old browsers, use simple escape
|
|
360
|
+
return `#${element.id.replace(/[^\w-]/g, '\\$&')}`;
|
|
361
|
+
}
|
|
315
362
|
}
|
|
316
363
|
|
|
317
|
-
// Try
|
|
318
|
-
if (element.
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
const selector = `${element.tagName.toLowerCase()}.${classes.join('.')}`;
|
|
364
|
+
// 2. Try data-testid (very common in modern apps)
|
|
365
|
+
if (element.dataset && element.dataset.testid && isSafeSelectorValue(element.dataset.testid)) {
|
|
366
|
+
const selector = `[data-testid="${element.dataset.testid}"]`;
|
|
367
|
+
try {
|
|
322
368
|
if (document.querySelectorAll(selector).length === 1) {
|
|
323
369
|
return selector;
|
|
324
370
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
371
|
+
} catch (e) {
|
|
372
|
+
// Invalid selector, continue
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 3. Try other data-* attributes (only if values are safe)
|
|
377
|
+
const dataAttrs = Array.from(element.attributes)
|
|
378
|
+
.filter(attr => attr.name.startsWith('data-') &&
|
|
379
|
+
attr.name !== 'data-testid' &&
|
|
380
|
+
isSafeSelectorValue(attr.value))
|
|
381
|
+
.slice(0, 2);
|
|
382
|
+
|
|
383
|
+
for (const attr of dataAttrs) {
|
|
384
|
+
const selector = `${tagName}[${attr.name}="${attr.value}"]`;
|
|
385
|
+
try {
|
|
386
|
+
if (document.querySelectorAll(selector).length === 1) {
|
|
387
|
+
return selector;
|
|
329
388
|
}
|
|
389
|
+
} catch (e) {
|
|
390
|
+
continue;
|
|
330
391
|
}
|
|
331
392
|
}
|
|
332
393
|
|
|
333
|
-
// Try
|
|
334
|
-
|
|
335
|
-
|
|
394
|
+
// 4. Try aria-label
|
|
395
|
+
const ariaLabel = element.getAttribute('aria-label');
|
|
396
|
+
if (ariaLabel && isSafeSelectorValue(ariaLabel)) {
|
|
397
|
+
const selector = `${tagName}[aria-label="${ariaLabel}"]`;
|
|
336
398
|
try {
|
|
337
399
|
if (document.querySelectorAll(selector).length === 1) {
|
|
338
400
|
return selector;
|
|
339
401
|
}
|
|
340
402
|
} catch (e) {
|
|
341
|
-
// Invalid selector,
|
|
403
|
+
// Invalid selector, continue
|
|
342
404
|
}
|
|
343
405
|
}
|
|
344
406
|
|
|
345
|
-
// Try
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
407
|
+
// 5. Try role attribute
|
|
408
|
+
const role = element.getAttribute('role');
|
|
409
|
+
if (role && isSafeSelectorValue(role)) {
|
|
410
|
+
const selector = `${tagName}[role="${role}"]`;
|
|
411
|
+
try {
|
|
412
|
+
if (document.querySelectorAll(selector).length === 1) {
|
|
413
|
+
return selector;
|
|
414
|
+
}
|
|
415
|
+
} catch (e) {
|
|
416
|
+
// Invalid selector, continue
|
|
417
|
+
}
|
|
418
|
+
}
|
|
349
419
|
|
|
350
|
-
|
|
351
|
-
|
|
420
|
+
// 6. Try name attribute (common for form inputs)
|
|
421
|
+
if (element.name && isSafeSelectorValue(element.name)) {
|
|
422
|
+
const selector = `${tagName}[name="${element.name}"]`;
|
|
352
423
|
try {
|
|
353
424
|
if (document.querySelectorAll(selector).length === 1) {
|
|
354
425
|
return selector;
|
|
355
426
|
}
|
|
356
427
|
} catch (e) {
|
|
357
|
-
// Invalid selector,
|
|
358
|
-
|
|
428
|
+
// Invalid selector, continue
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 7. Try semantic classes (filter out Tailwind)
|
|
433
|
+
if (element.className && typeof element.className === 'string') {
|
|
434
|
+
const classes = element.className.split(' ')
|
|
435
|
+
.filter(c => c.trim() && !isTailwindClass(c))
|
|
436
|
+
.slice(0, 3); // Limit to 3 classes max
|
|
437
|
+
|
|
438
|
+
if (classes.length > 0) {
|
|
439
|
+
try {
|
|
440
|
+
// Try with all filtered classes
|
|
441
|
+
const escapedClasses = classes.map(c => {
|
|
442
|
+
try {
|
|
443
|
+
return CSS.escape(c);
|
|
444
|
+
} catch (e) {
|
|
445
|
+
return c.replace(/[^\w-]/g, '\\$&');
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
const selector = `${tagName}.${escapedClasses.join('.')}`;
|
|
449
|
+
if (document.querySelectorAll(selector).length === 1) {
|
|
450
|
+
return selector;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Try with first class only
|
|
454
|
+
const firstClassSelector = `${tagName}.${escapedClasses[0]}`;
|
|
455
|
+
if (document.querySelectorAll(firstClassSelector).length === 1) {
|
|
456
|
+
return firstClassSelector;
|
|
457
|
+
}
|
|
458
|
+
} catch (e) {
|
|
459
|
+
// Invalid selector, continue to fallback
|
|
460
|
+
}
|
|
359
461
|
}
|
|
360
462
|
}
|
|
361
463
|
|
|
362
|
-
// Fallback: nth-
|
|
464
|
+
// 8. Fallback: nth-of-type with path
|
|
363
465
|
let current = element;
|
|
364
466
|
const path = [];
|
|
365
467
|
|
|
366
468
|
while (current && current.tagName) {
|
|
367
469
|
let selector = current.tagName.toLowerCase();
|
|
368
470
|
|
|
369
|
-
|
|
370
|
-
|
|
471
|
+
// Stop at element with ID
|
|
472
|
+
if (current.id && isSafeSelectorValue(current.id)) {
|
|
473
|
+
try {
|
|
474
|
+
selector = `#${CSS.escape(current.id)}`;
|
|
475
|
+
} catch (e) {
|
|
476
|
+
selector = `#${current.id.replace(/[^\w-]/g, '\\$&')}`;
|
|
477
|
+
}
|
|
371
478
|
path.unshift(selector);
|
|
372
479
|
break;
|
|
373
480
|
}
|
|
374
481
|
|
|
482
|
+
// Calculate nth-of-type
|
|
375
483
|
let sibling = current;
|
|
376
484
|
let nth = 1;
|
|
377
485
|
|
|
@@ -382,7 +490,9 @@ function getUniqueSelectorInPage(element) {
|
|
|
382
490
|
}
|
|
383
491
|
}
|
|
384
492
|
|
|
385
|
-
|
|
493
|
+
// Only add nth-of-type if there are multiple siblings of same type
|
|
494
|
+
if (nth > 1 || (current.parentElement &&
|
|
495
|
+
Array.from(current.parentElement.children).filter(c => c.tagName === current.tagName).length > 1)) {
|
|
386
496
|
selector += `:nth-of-type(${nth})`;
|
|
387
497
|
}
|
|
388
498
|
|
package/figma-tools.js
CHANGED
|
@@ -377,3 +377,123 @@ export function collectAllText(node, texts = []) {
|
|
|
377
377
|
}
|
|
378
378
|
return texts;
|
|
379
379
|
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Simplify Figma node structure for code generation
|
|
383
|
+
* Extracts only essential properties: layout, styling, text, and children
|
|
384
|
+
*/
|
|
385
|
+
export function simplifyNode(node) {
|
|
386
|
+
if (!node) return null;
|
|
387
|
+
|
|
388
|
+
const simplified = {
|
|
389
|
+
type: node.type,
|
|
390
|
+
name: node.name,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// Dimensions
|
|
394
|
+
if (node.absoluteBoundingBox) {
|
|
395
|
+
simplified.size = {
|
|
396
|
+
width: Math.round(node.absoluteBoundingBox.width),
|
|
397
|
+
height: Math.round(node.absoluteBoundingBox.height),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Layout properties (Auto Layout / Flexbox)
|
|
402
|
+
if (node.layoutMode) {
|
|
403
|
+
simplified.layout = {
|
|
404
|
+
mode: node.layoutMode, // HORIZONTAL or VERTICAL
|
|
405
|
+
padding: (node.paddingLeft || node.paddingTop || node.paddingRight || node.paddingBottom) ? {
|
|
406
|
+
top: node.paddingTop || 0,
|
|
407
|
+
right: node.paddingRight || 0,
|
|
408
|
+
bottom: node.paddingBottom || 0,
|
|
409
|
+
left: node.paddingLeft || 0,
|
|
410
|
+
} : undefined,
|
|
411
|
+
gap: node.itemSpacing,
|
|
412
|
+
align: node.primaryAxisAlignItems,
|
|
413
|
+
justify: node.counterAxisAlignItems,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Border radius
|
|
418
|
+
if (node.cornerRadius) {
|
|
419
|
+
simplified.borderRadius = node.cornerRadius;
|
|
420
|
+
} else if (node.rectangleCornerRadii) {
|
|
421
|
+
simplified.borderRadius = node.rectangleCornerRadii;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Fills (backgrounds)
|
|
425
|
+
if (node.fills && node.fills.length > 0) {
|
|
426
|
+
simplified.fills = node.fills
|
|
427
|
+
.filter(fill => fill.visible !== false)
|
|
428
|
+
.map(fill => ({
|
|
429
|
+
type: fill.type,
|
|
430
|
+
color: fill.color ? {
|
|
431
|
+
r: Math.round(fill.color.r * 255),
|
|
432
|
+
g: Math.round(fill.color.g * 255),
|
|
433
|
+
b: Math.round(fill.color.b * 255),
|
|
434
|
+
a: fill.color.a !== undefined ? Math.round(fill.color.a * 100) / 100 : 1,
|
|
435
|
+
} : undefined,
|
|
436
|
+
opacity: fill.opacity,
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Strokes (borders)
|
|
441
|
+
if (node.strokes && node.strokes.length > 0) {
|
|
442
|
+
simplified.strokes = node.strokes
|
|
443
|
+
.filter(stroke => stroke.visible !== false)
|
|
444
|
+
.map(stroke => ({
|
|
445
|
+
type: stroke.type,
|
|
446
|
+
color: stroke.color ? {
|
|
447
|
+
r: Math.round(stroke.color.r * 255),
|
|
448
|
+
g: Math.round(stroke.color.g * 255),
|
|
449
|
+
b: Math.round(stroke.color.b * 255),
|
|
450
|
+
a: stroke.color.a !== undefined ? Math.round(stroke.color.a * 100) / 100 : 1,
|
|
451
|
+
} : undefined,
|
|
452
|
+
}));
|
|
453
|
+
|
|
454
|
+
if (node.strokeWeight) {
|
|
455
|
+
simplified.strokeWeight = node.strokeWeight;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Effects (shadows, blurs)
|
|
460
|
+
if (node.effects && node.effects.length > 0) {
|
|
461
|
+
simplified.effects = node.effects
|
|
462
|
+
.filter(effect => effect.visible !== false)
|
|
463
|
+
.map(effect => ({
|
|
464
|
+
type: effect.type,
|
|
465
|
+
radius: effect.radius,
|
|
466
|
+
offset: effect.offset,
|
|
467
|
+
color: effect.color ? {
|
|
468
|
+
r: Math.round(effect.color.r * 255),
|
|
469
|
+
g: Math.round(effect.color.g * 255),
|
|
470
|
+
b: Math.round(effect.color.b * 255),
|
|
471
|
+
a: Math.round(effect.color.a * 100) / 100,
|
|
472
|
+
} : undefined,
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Text properties
|
|
477
|
+
if (node.type === 'TEXT') {
|
|
478
|
+
simplified.text = node.characters;
|
|
479
|
+
if (node.style) {
|
|
480
|
+
simplified.textStyle = {
|
|
481
|
+
fontFamily: node.style.fontFamily,
|
|
482
|
+
fontWeight: node.style.fontWeight,
|
|
483
|
+
fontSize: node.style.fontSize,
|
|
484
|
+
lineHeight: node.style.lineHeightPx,
|
|
485
|
+
letterSpacing: node.style.letterSpacing,
|
|
486
|
+
textAlign: node.style.textAlignHorizontal,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Recursively simplify children
|
|
492
|
+
if (node.children && node.children.length > 0) {
|
|
493
|
+
simplified.children = node.children
|
|
494
|
+
.map(child => simplifyNode(child))
|
|
495
|
+
.filter(Boolean); // Remove null values
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return simplified;
|
|
499
|
+
}
|