figma-console-mcp 1.22.0 β 1.22.1
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/README.md +2 -1
- package/figma-desktop-bridge/code.js +174 -60
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
> **Your design system as an API.** Model Context Protocol server that bridges design and developmentβgiving AI assistants complete access to Figma for **extraction**, **creation**, and **debugging**.
|
|
10
10
|
|
|
11
|
-
> **π
|
|
11
|
+
> **π Comprehensive Accessibility Scanning (v1.22.0):** Full-spectrum WCAG coverage across design and code β 13 design-side lint rules, component accessibility scorecards with color-blind simulation, code-side scanning via axe-core (104 rules), and design-to-code accessibility parity checking. No rule database to maintain. [See what's new β](docs/figma-mcp-vs-figma-console-mcp.md)
|
|
12
12
|
|
|
13
13
|
## What is this?
|
|
14
14
|
|
|
@@ -21,6 +21,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
|
21
21
|
- **π§ Variable management** - Create, update, rename, and delete design tokens
|
|
22
22
|
- **β‘ Real-time monitoring** - Watch logs as plugins execute
|
|
23
23
|
- **π FigJam boards** - Create stickies, flowcharts, tables, and code blocks on collaborative boards
|
|
24
|
+
- **βΏ Accessibility scanning** - 13 WCAG design checks, component scorecards, axe-core code scanning, design-to-code parity
|
|
24
25
|
- **βοΈ Cloud Write Relay** - Web AI clients (Claude.ai, v0, Replit) can design in Figma via cloud pairing
|
|
25
26
|
- **π Four ways to connect** - Remote SSE, Cloud Mode, NPX, or Local Git
|
|
26
27
|
|
|
@@ -4430,7 +4430,47 @@ figma.ui.onmessage = async (msg) => {
|
|
|
4430
4430
|
var variants = isComponentSet ? componentSet.children : [componentSet];
|
|
4431
4431
|
var variantCount = variants.length;
|
|
4432
4432
|
|
|
4433
|
-
//
|
|
4433
|
+
// ---- Classify component as interactive vs presentational ----
|
|
4434
|
+
var interactiveNames = /^(button|link|input|checkbox|radio|switch|toggle|tab|select|slider|dropdown|menu-item|search|combobox|listbox)/i;
|
|
4435
|
+
var presentationalNames = /^(alert|badge|card|avatar|divider|skeleton|tooltip|tag|chip|banner|callout|notification|toast|icon|image|separator|progress|spinner|loader|breadcrumb|label|heading|paragraph|caption|stat|meter|indicator)/i;
|
|
4436
|
+
|
|
4437
|
+
// Parse variant axes from variant names (e.g., "type=success, style=fill" β {type: [...], style: [...]})
|
|
4438
|
+
var variantAxes = {};
|
|
4439
|
+
var hasStateAxis = false;
|
|
4440
|
+
for (var vai = 0; vai < variants.length; vai++) {
|
|
4441
|
+
var vParts = variants[vai].name.split(',');
|
|
4442
|
+
for (var vpi = 0; vpi < vParts.length; vpi++) {
|
|
4443
|
+
var kv = vParts[vpi].trim().split('=');
|
|
4444
|
+
if (kv.length === 2) {
|
|
4445
|
+
var axisName = kv[0].trim().toLowerCase();
|
|
4446
|
+
var axisValue = kv[1].trim().toLowerCase();
|
|
4447
|
+
if (!variantAxes[axisName]) variantAxes[axisName] = [];
|
|
4448
|
+
if (variantAxes[axisName].indexOf(axisValue) === -1) {
|
|
4449
|
+
variantAxes[axisName].push(axisValue);
|
|
4450
|
+
}
|
|
4451
|
+
// Check if this axis contains interaction state values
|
|
4452
|
+
if (axisName === 'state' && /(hover|focus|pressed|disabled|active)/i.test(axisValue)) {
|
|
4453
|
+
hasStateAxis = true;
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
var componentName = componentSet.name || '';
|
|
4460
|
+
var isInteractive = interactiveNames.test(componentName) || hasStateAxis;
|
|
4461
|
+
var isPresentational = !isInteractive && (presentationalNames.test(componentName) || !hasStateAxis);
|
|
4462
|
+
// If ambiguous, check if any variant mentions interaction states
|
|
4463
|
+
if (!isInteractive && !isPresentational) {
|
|
4464
|
+
for (var ami = 0; ami < variants.length; ami++) {
|
|
4465
|
+
if (/(hover|focus|pressed|disabled)/i.test(variants[ami].name)) {
|
|
4466
|
+
isInteractive = true;
|
|
4467
|
+
break;
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
if (!isInteractive) isPresentational = true;
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
// 1. Coverage analysis β adapts to component classification
|
|
4434
4474
|
var stateKeywords = {
|
|
4435
4475
|
'default': /(default|rest|idle|normal|base)/i,
|
|
4436
4476
|
'hover': /(hover|hovered)/i,
|
|
@@ -4440,35 +4480,69 @@ figma.ui.onmessage = async (msg) => {
|
|
|
4440
4480
|
'active': /(active|pressed|selected)/i,
|
|
4441
4481
|
'loading': /(loading|spinner)/i
|
|
4442
4482
|
};
|
|
4483
|
+
|
|
4484
|
+
var coveredCount = 0;
|
|
4485
|
+
var totalStates = 0;
|
|
4486
|
+
var missingStates = [];
|
|
4443
4487
|
var statesCovered = {};
|
|
4444
4488
|
var statesFound = {};
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
for (var
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4489
|
+
var coverageLabel = '';
|
|
4490
|
+
var variantAxisCoverage = null;
|
|
4491
|
+
|
|
4492
|
+
if (isInteractive) {
|
|
4493
|
+
// Interactive components: check for interaction states
|
|
4494
|
+
coverageLabel = 'interactive-states';
|
|
4495
|
+
for (var sk in stateKeywords) {
|
|
4496
|
+
statesCovered[sk] = false;
|
|
4497
|
+
statesFound[sk] = null;
|
|
4498
|
+
}
|
|
4499
|
+
for (var vi = 0; vi < variants.length; vi++) {
|
|
4500
|
+
var vName = variants[vi].name;
|
|
4501
|
+
for (var sk2 in stateKeywords) {
|
|
4502
|
+
if (stateKeywords[sk2].test(vName)) {
|
|
4503
|
+
statesCovered[sk2] = true;
|
|
4504
|
+
if (!statesFound[sk2]) statesFound[sk2] = vName;
|
|
4505
|
+
}
|
|
4455
4506
|
}
|
|
4456
4507
|
}
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4508
|
+
if (variantCount === 1 && !statesCovered['default']) {
|
|
4509
|
+
statesCovered['default'] = true;
|
|
4510
|
+
statesFound['default'] = variants[0].name;
|
|
4511
|
+
}
|
|
4512
|
+
for (var sk3 in statesCovered) {
|
|
4513
|
+
totalStates++;
|
|
4514
|
+
if (statesCovered[sk3]) {
|
|
4515
|
+
coveredCount++;
|
|
4516
|
+
} else {
|
|
4517
|
+
missingStates.push(sk3);
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
} else {
|
|
4521
|
+
// Presentational components: check variant axis completeness
|
|
4522
|
+
coverageLabel = 'variant-axes';
|
|
4523
|
+
// Calculate expected combinations vs actual
|
|
4524
|
+
var axisNames = [];
|
|
4525
|
+
var axisCounts = [];
|
|
4526
|
+
var expectedCombinations = 1;
|
|
4527
|
+
for (var axName in variantAxes) {
|
|
4528
|
+
axisNames.push(axName);
|
|
4529
|
+
axisCounts.push(variantAxes[axName].length);
|
|
4530
|
+
expectedCombinations *= variantAxes[axName].length;
|
|
4531
|
+
}
|
|
4532
|
+
// Score: actual variants / expected combinations (capped at 100%)
|
|
4533
|
+
var axisCoverageRatio = expectedCombinations > 0 ? Math.min(1, variantCount / expectedCombinations) : 1;
|
|
4534
|
+
coveredCount = variantCount;
|
|
4535
|
+
totalStates = expectedCombinations;
|
|
4536
|
+
variantAxisCoverage = {
|
|
4537
|
+
axes: variantAxes,
|
|
4538
|
+
axisCount: axisNames.length,
|
|
4539
|
+
expectedCombinations: expectedCombinations,
|
|
4540
|
+
actualVariants: variantCount,
|
|
4541
|
+
completeness: Math.round(axisCoverageRatio * 100) + '%'
|
|
4542
|
+
};
|
|
4543
|
+
// For presentational, no "missing states" β instead note if combinations are incomplete
|
|
4544
|
+
if (variantCount < expectedCombinations) {
|
|
4545
|
+
missingStates.push(variantCount + '/' + expectedCombinations + ' axis combinations present');
|
|
4472
4546
|
}
|
|
4473
4547
|
}
|
|
4474
4548
|
|
|
@@ -4552,6 +4626,9 @@ figma.ui.onmessage = async (msg) => {
|
|
|
4552
4626
|
}
|
|
4553
4627
|
|
|
4554
4628
|
// 4. Target size analysis
|
|
4629
|
+
// WCAG 2.5.8 applies to interactive targets β presentational components
|
|
4630
|
+
// (badges, avatars, progress bars) are not tap targets by definition.
|
|
4631
|
+
// Skip target size checking for presentational components to avoid false positives.
|
|
4555
4632
|
var targetSizeAnalysis = { minWidth: Infinity, minHeight: Infinity, variants: [], issues: [] };
|
|
4556
4633
|
var minTarget = msg.targetSize || 24; // Default WCAG 2.5.8 minimum
|
|
4557
4634
|
for (var tvi = 0; tvi < variants.length; tvi++) {
|
|
@@ -4562,7 +4639,8 @@ figma.ui.onmessage = async (msg) => {
|
|
|
4562
4639
|
targetSizeAnalysis.variants.push({ name: variants[tvi].name, width: tw, height: th });
|
|
4563
4640
|
if (tw < targetSizeAnalysis.minWidth) targetSizeAnalysis.minWidth = tw;
|
|
4564
4641
|
if (th < targetSizeAnalysis.minHeight) targetSizeAnalysis.minHeight = th;
|
|
4565
|
-
|
|
4642
|
+
// Only flag target size issues for interactive components
|
|
4643
|
+
if (isInteractive && (tw < minTarget || th < minTarget)) {
|
|
4566
4644
|
targetSizeAnalysis.issues.push({
|
|
4567
4645
|
variant: variants[tvi].name,
|
|
4568
4646
|
width: tw,
|
|
@@ -4658,14 +4736,15 @@ figma.ui.onmessage = async (msg) => {
|
|
|
4658
4736
|
|
|
4659
4737
|
// ---- Compute overall score ----
|
|
4660
4738
|
var scores = {};
|
|
4661
|
-
//
|
|
4662
|
-
scores.
|
|
4739
|
+
// Coverage: percentage of states (interactive) or axis combinations (presentational)
|
|
4740
|
+
scores.variantCoverage = totalStates > 0 ? Math.round((coveredCount / totalStates) * 100) : 100;
|
|
4663
4741
|
// Focus indicator: 0 (missing), 50 (exists but no indicator), 100 (good indicator)
|
|
4664
|
-
|
|
4742
|
+
// For presentational: N/A β score 100 (don't penalize)
|
|
4743
|
+
scores.focusIndicator = isPresentational ? 100 : (!focusAnalysis.hasVariant ? 0 : (!focusAnalysis.hasVisibleIndicator ? 50 : 100));
|
|
4665
4744
|
// Color differentiation: 100 if no issues, decremented per issue
|
|
4666
4745
|
scores.colorDifferentiation = colorDifferentiation.checked === 0 ? 100 : Math.max(0, Math.round(((colorDifferentiation.checked - colorDifferentiation.issues.length) / colorDifferentiation.checked) * 100));
|
|
4667
|
-
// Target size:
|
|
4668
|
-
scores.targetSize = targetSizeAnalysis.issues.length === 0 ? 100 : Math.max(0, Math.round(((targetSizeAnalysis.variants.length - targetSizeAnalysis.issues.length) / Math.max(1, targetSizeAnalysis.variants.length)) * 100));
|
|
4746
|
+
// Target size: N/A for presentational (not tap targets), scored for interactive
|
|
4747
|
+
scores.targetSize = isPresentational ? 100 : (targetSizeAnalysis.issues.length === 0 ? 100 : Math.max(0, Math.round(((targetSizeAnalysis.variants.length - targetSizeAnalysis.issues.length) / Math.max(1, targetSizeAnalysis.variants.length)) * 100)));
|
|
4669
4748
|
// Annotations: 0 (nothing), 50 (description only), 100 (has a11y notes)
|
|
4670
4749
|
scores.annotations = annotations.hasA11yNotes ? 100 : (annotations.hasDescription ? 50 : 0);
|
|
4671
4750
|
// Color blind: percentage of simulations with no issues
|
|
@@ -4675,48 +4754,76 @@ figma.ui.onmessage = async (msg) => {
|
|
|
4675
4754
|
}
|
|
4676
4755
|
scores.colorBlindSafety = colorBlindAnalysis.simulations.length > 0 ? Math.round((cbPassCount / colorBlindAnalysis.simulations.length) * 100) : 100;
|
|
4677
4756
|
|
|
4678
|
-
// Overall weighted score
|
|
4679
|
-
var overall
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4757
|
+
// Overall weighted score β weights differ by component classification
|
|
4758
|
+
var overall;
|
|
4759
|
+
if (isInteractive) {
|
|
4760
|
+
// Interactive: focus and states matter most
|
|
4761
|
+
overall = Math.round(
|
|
4762
|
+
scores.variantCoverage * 0.20 +
|
|
4763
|
+
scores.focusIndicator * 0.20 +
|
|
4764
|
+
scores.colorDifferentiation * 0.15 +
|
|
4765
|
+
scores.targetSize * 0.15 +
|
|
4766
|
+
scores.annotations * 0.10 +
|
|
4767
|
+
scores.colorBlindSafety * 0.20
|
|
4768
|
+
);
|
|
4769
|
+
} else {
|
|
4770
|
+
// Presentational: variant completeness and color safety matter most, focus is N/A
|
|
4771
|
+
overall = Math.round(
|
|
4772
|
+
scores.variantCoverage * 0.25 +
|
|
4773
|
+
scores.colorDifferentiation * 0.25 +
|
|
4774
|
+
scores.annotations * 0.15 +
|
|
4775
|
+
scores.colorBlindSafety * 0.25 +
|
|
4776
|
+
scores.targetSize * 0.10
|
|
4777
|
+
);
|
|
4778
|
+
}
|
|
4687
4779
|
|
|
4688
4780
|
// ---- Build response ----
|
|
4781
|
+
var coverageSection;
|
|
4782
|
+
if (isInteractive) {
|
|
4783
|
+
coverageSection = {
|
|
4784
|
+
mode: 'interactive-states',
|
|
4785
|
+
found: statesFound,
|
|
4786
|
+
missing: missingStates,
|
|
4787
|
+
coverage: coveredCount + '/' + totalStates
|
|
4788
|
+
};
|
|
4789
|
+
} else {
|
|
4790
|
+
coverageSection = {
|
|
4791
|
+
mode: 'variant-axes',
|
|
4792
|
+
axes: variantAxisCoverage,
|
|
4793
|
+
coverage: coveredCount + '/' + totalStates
|
|
4794
|
+
};
|
|
4795
|
+
}
|
|
4796
|
+
|
|
4689
4797
|
var auditResult = {
|
|
4690
4798
|
component: {
|
|
4691
4799
|
id: componentSet.id,
|
|
4692
4800
|
name: componentSet.name,
|
|
4693
4801
|
type: componentSet.type,
|
|
4694
|
-
variantCount: variantCount
|
|
4802
|
+
variantCount: variantCount,
|
|
4803
|
+
classification: isInteractive ? 'interactive' : 'presentational'
|
|
4695
4804
|
},
|
|
4696
4805
|
overallScore: overall,
|
|
4697
4806
|
scores: scores,
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
missing: missingStates,
|
|
4701
|
-
coverage: coveredCount + '/' + totalStates
|
|
4702
|
-
},
|
|
4703
|
-
focusIndicator: focusAnalysis,
|
|
4807
|
+
variantCoverage: coverageSection,
|
|
4808
|
+
focusIndicator: isInteractive ? focusAnalysis : { notApplicable: true, details: 'Focus indicators are not expected for presentational components' },
|
|
4704
4809
|
colorDifferentiation: colorDifferentiation,
|
|
4705
|
-
targetSize: {
|
|
4810
|
+
targetSize: isInteractive ? {
|
|
4706
4811
|
minimum: minTarget + 'x' + minTarget,
|
|
4707
4812
|
smallest: targetSizeAnalysis.minWidth + 'x' + targetSizeAnalysis.minHeight,
|
|
4708
4813
|
issues: targetSizeAnalysis.issues
|
|
4709
|
-
},
|
|
4814
|
+
} : { notApplicable: true, details: 'Target size checks apply to interactive components (WCAG 2.5.8 is about tap targets)', smallest: targetSizeAnalysis.minWidth + 'x' + targetSizeAnalysis.minHeight },
|
|
4710
4815
|
annotations: annotations,
|
|
4711
4816
|
colorBlindSimulation: colorBlindAnalysis,
|
|
4712
4817
|
recommendations: []
|
|
4713
4818
|
};
|
|
4714
4819
|
|
|
4715
|
-
// Generate recommendations
|
|
4716
|
-
if (
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4820
|
+
// Generate recommendations β classification-aware
|
|
4821
|
+
if (isInteractive) {
|
|
4822
|
+
if (!focusAnalysis.hasVariant) {
|
|
4823
|
+
auditResult.recommendations.push({ priority: 'high', area: 'focus', message: 'Add a focus/focused variant with a visible focus ring (WCAG 2.4.7)' });
|
|
4824
|
+
} else if (!focusAnalysis.hasVisibleIndicator) {
|
|
4825
|
+
auditResult.recommendations.push({ priority: 'medium', area: 'focus', message: 'Focus variant exists but lacks visible indicator β add a border or shadow' });
|
|
4826
|
+
}
|
|
4720
4827
|
}
|
|
4721
4828
|
if (colorDifferentiation.issues.length > 0) {
|
|
4722
4829
|
auditResult.recommendations.push({ priority: 'high', area: 'color', message: 'Add non-color indicators (icons, borders, text) to ' + colorDifferentiation.issues.length + ' state variant(s) (WCAG 1.4.1)' });
|
|
@@ -4728,16 +4835,23 @@ figma.ui.onmessage = async (msg) => {
|
|
|
4728
4835
|
auditResult.recommendations.push({ priority: 'medium', area: 'documentation', message: 'Add a component description with usage guidelines' });
|
|
4729
4836
|
}
|
|
4730
4837
|
if (!annotations.hasA11yNotes) {
|
|
4731
|
-
|
|
4838
|
+
var a11yHint = isInteractive
|
|
4839
|
+
? 'Add accessibility notes (ARIA role, keyboard interactions, screen reader behavior)'
|
|
4840
|
+
: 'Add accessibility notes (ARIA role, live region behavior, semantic usage)';
|
|
4841
|
+
auditResult.recommendations.push({ priority: 'medium', area: 'documentation', message: a11yHint });
|
|
4732
4842
|
}
|
|
4733
4843
|
if (colorBlindAnalysis.issues.length > 0) {
|
|
4734
4844
|
auditResult.recommendations.push({ priority: 'medium', area: 'color-blind', message: colorBlindAnalysis.issues.join('; ') });
|
|
4735
4845
|
}
|
|
4736
|
-
|
|
4737
|
-
var
|
|
4738
|
-
|
|
4739
|
-
|
|
4846
|
+
if (isInteractive) {
|
|
4847
|
+
for (var msi = 0; msi < missingStates.length; msi++) {
|
|
4848
|
+
var ms = missingStates[msi];
|
|
4849
|
+
if (ms === 'focus' || ms === 'disabled') {
|
|
4850
|
+
auditResult.recommendations.push({ priority: 'medium', area: 'states', message: 'Consider adding a "' + ms + '" variant for complete interactive state coverage' });
|
|
4851
|
+
}
|
|
4740
4852
|
}
|
|
4853
|
+
} else if (variantAxisCoverage && variantCount < variantAxisCoverage.expectedCombinations) {
|
|
4854
|
+
auditResult.recommendations.push({ priority: 'low', area: 'coverage', message: variantCount + ' of ' + variantAxisCoverage.expectedCombinations + ' axis combinations present β consider adding missing variants for completeness' });
|
|
4741
4855
|
}
|
|
4742
4856
|
|
|
4743
4857
|
console.log('π [Desktop Bridge] Component audit complete: score ' + overall + '/100 for "' + componentSet.name + '"');
|
package/package.json
CHANGED