@mp3wizard/figma-console-mcp 1.21.2 → 1.22.2
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 +10 -9
- package/dist/apps/design-system-dashboard/mcp-app.html +59 -59
- package/dist/apps/token-browser/mcp-app.html +53 -53
- package/dist/cloudflare/core/accessibility-tools.js +306 -0
- package/dist/cloudflare/core/cloud-websocket-connector.js +11 -0
- package/dist/cloudflare/core/design-code-tools.js +160 -2
- package/dist/cloudflare/core/figma-desktop-connector.js +2 -0
- package/dist/cloudflare/core/websocket-connector.js +11 -0
- package/dist/cloudflare/core/write-tools.js +49 -4
- package/dist/cloudflare/index.js +16 -7
- package/dist/core/accessibility-tools.d.ts +21 -0
- package/dist/core/accessibility-tools.d.ts.map +1 -0
- package/dist/core/accessibility-tools.js +307 -0
- package/dist/core/accessibility-tools.js.map +1 -0
- package/dist/core/design-code-tools.d.ts.map +1 -1
- package/dist/core/design-code-tools.js +160 -2
- package/dist/core/design-code-tools.js.map +1 -1
- package/dist/core/figma-connector.d.ts +1 -0
- package/dist/core/figma-connector.d.ts.map +1 -1
- package/dist/core/figma-desktop-connector.d.ts +1 -0
- package/dist/core/figma-desktop-connector.d.ts.map +1 -1
- package/dist/core/figma-desktop-connector.js +2 -0
- package/dist/core/figma-desktop-connector.js.map +1 -1
- package/dist/core/types/design-code.d.ts +8 -0
- package/dist/core/types/design-code.d.ts.map +1 -1
- package/dist/core/websocket-connector.d.ts +1 -0
- package/dist/core/websocket-connector.d.ts.map +1 -1
- package/dist/core/websocket-connector.js +11 -0
- package/dist/core/websocket-connector.js.map +1 -1
- package/dist/core/write-tools.d.ts.map +1 -1
- package/dist/core/write-tools.js +49 -4
- package/dist/core/write-tools.js.map +1 -1
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +52 -4
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/code.js +1134 -1
- package/figma-desktop-bridge/ui-full.html +13 -0
- package/figma-desktop-bridge/ui.html +13 -0
- package/package.json +5 -101
package/dist/cloudflare/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { registerCommentTools } from "./core/comment-tools.js";
|
|
|
23
23
|
import { registerAnnotationTools } from "./core/annotation-tools.js";
|
|
24
24
|
import { registerDeepComponentTools } from "./core/deep-component-tools.js";
|
|
25
25
|
import { registerDesignSystemTools } from "./core/design-system-tools.js";
|
|
26
|
+
import { registerAccessibilityTools } from "./core/accessibility-tools.js";
|
|
26
27
|
import { generatePairingCode } from "./core/cloud-websocket-relay.js";
|
|
27
28
|
import { CloudWebSocketConnector } from "./core/cloud-websocket-connector.js";
|
|
28
29
|
import { registerWriteTools } from "./core/write-tools.js";
|
|
@@ -68,7 +69,7 @@ export class FigmaConsoleMCPv3 extends McpAgent {
|
|
|
68
69
|
super(...arguments);
|
|
69
70
|
this.server = new McpServer({
|
|
70
71
|
name: "Figma Console MCP",
|
|
71
|
-
version: "1.
|
|
72
|
+
version: "1.22.0",
|
|
72
73
|
});
|
|
73
74
|
this.browserManager = null;
|
|
74
75
|
this.consoleMonitor = null;
|
|
@@ -795,6 +796,14 @@ export class FigmaConsoleMCPv3 extends McpAgent {
|
|
|
795
796
|
// Register Design System Kit tool
|
|
796
797
|
registerDesignSystemTools(this.server, async () => await this.getFigmaAPI(), () => this.browserManager?.getCurrentUrl() || null, undefined, // variablesCache
|
|
797
798
|
{ isRemoteMode: true });
|
|
799
|
+
// Register code-side accessibility scanning (axe-core + JSDOM)
|
|
800
|
+
// Note: May not work in Cloudflare Workers due to JSDOM dependency
|
|
801
|
+
try {
|
|
802
|
+
registerAccessibilityTools(this.server);
|
|
803
|
+
}
|
|
804
|
+
catch (e) {
|
|
805
|
+
// Silently skip if axe-core/jsdom not available in Workers environment
|
|
806
|
+
}
|
|
798
807
|
// Note: MCP Apps (Token Browser, Dashboard) are registered in local.ts only
|
|
799
808
|
// They require Node.js file system APIs that don't work in Cloudflare Workers
|
|
800
809
|
}
|
|
@@ -1046,7 +1055,7 @@ export default {
|
|
|
1046
1055
|
});
|
|
1047
1056
|
const statelessServer = new McpServer({
|
|
1048
1057
|
name: "Figma Console MCP",
|
|
1049
|
-
version: "1.
|
|
1058
|
+
version: "1.22.0",
|
|
1050
1059
|
});
|
|
1051
1060
|
// ================================================================
|
|
1052
1061
|
// Cloud Write Relay — Pairing Tool (stateless /mcp path)
|
|
@@ -1681,7 +1690,7 @@ export default {
|
|
|
1681
1690
|
return new Response(JSON.stringify({
|
|
1682
1691
|
status: "healthy",
|
|
1683
1692
|
service: "Figma Console MCP",
|
|
1684
|
-
version: "1.
|
|
1693
|
+
version: "1.22.0",
|
|
1685
1694
|
endpoints: {
|
|
1686
1695
|
mcp: ["/sse", "/mcp"],
|
|
1687
1696
|
oauth_mcp_spec: ["/.well-known/oauth-authorization-server", "/authorize", "/token", "/oauth/register"],
|
|
@@ -1727,13 +1736,13 @@ export default {
|
|
|
1727
1736
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1728
1737
|
<title>Figma Console MCP - The Most Comprehensive MCP Server for Figma</title>
|
|
1729
1738
|
<link rel="icon" type="image/svg+xml" href="https://docs.figma-console-mcp.southleft.com/favicon.svg">
|
|
1730
|
-
<meta name="description" content="Turn your Figma design system into a living API.
|
|
1739
|
+
<meta name="description" content="Turn your Figma design system into a living API. 94+ tools give AI assistants deep access to design tokens, component specs, variables, and programmatic design creation.">
|
|
1731
1740
|
|
|
1732
1741
|
<!-- Open Graph -->
|
|
1733
1742
|
<meta property="og:type" content="website">
|
|
1734
1743
|
<meta property="og:url" content="https://figma-console-mcp.southleft.com">
|
|
1735
1744
|
<meta property="og:title" content="Figma Console MCP - Turn Your Design System Into a Living API">
|
|
1736
|
-
<meta property="og:description" content="The most comprehensive MCP server for Figma.
|
|
1745
|
+
<meta property="og:description" content="The most comprehensive MCP server for Figma. 94+ tools give AI assistants deep access to design tokens, components, variables, and programmatic design creation.">
|
|
1737
1746
|
<meta property="og:image" content="https://docs.figma-console-mcp.southleft.com/images/og-image.jpg">
|
|
1738
1747
|
<meta property="og:image:width" content="1200">
|
|
1739
1748
|
<meta property="og:image:height" content="630">
|
|
@@ -1741,7 +1750,7 @@ export default {
|
|
|
1741
1750
|
<!-- Twitter -->
|
|
1742
1751
|
<meta name="twitter:card" content="summary_large_image">
|
|
1743
1752
|
<meta name="twitter:title" content="Figma Console MCP - Turn Your Design System Into a Living API">
|
|
1744
|
-
<meta name="twitter:description" content="The most comprehensive MCP server for Figma.
|
|
1753
|
+
<meta name="twitter:description" content="The most comprehensive MCP server for Figma. 94+ tools give AI assistants deep access to design tokens, components, variables, and programmatic design creation.">
|
|
1745
1754
|
<meta name="twitter:image" content="https://docs.figma-console-mcp.southleft.com/images/og-image.jpg">
|
|
1746
1755
|
|
|
1747
1756
|
<meta name="theme-color" content="#0D9488">
|
|
@@ -2628,7 +2637,7 @@ export default {
|
|
|
2628
2637
|
<div class="grid-cell showcase-cell rule-left">
|
|
2629
2638
|
<div class="showcase-label">What AI Can Access</div>
|
|
2630
2639
|
<div class="showcase-stat">
|
|
2631
|
-
<span class="number">
|
|
2640
|
+
<span class="number">94+</span>
|
|
2632
2641
|
<span class="label">MCP tools for Figma</span>
|
|
2633
2642
|
</div>
|
|
2634
2643
|
<div class="capability-list">
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code-side accessibility scanning via axe-core + JSDOM.
|
|
3
|
+
*
|
|
4
|
+
* Delegates all rule logic to axe-core (Deque) — the MCP never owns
|
|
5
|
+
* a rule database. JSDOM provides a lightweight DOM for structural checks
|
|
6
|
+
* (~50 rules: ARIA, semantics, alt text, form labels, headings, landmarks).
|
|
7
|
+
*
|
|
8
|
+
* Visual rules (color contrast, focus-visible) are NOT available via JSDOM —
|
|
9
|
+
* those are handled by the design-side figma_lint_design tool.
|
|
10
|
+
*/
|
|
11
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
|
+
/**
|
|
13
|
+
* Extract a CodeSpec.accessibility object from HTML + axe-core results.
|
|
14
|
+
* This bridges Phase 3 (code scanning) → Phase 4 (parity comparison).
|
|
15
|
+
*
|
|
16
|
+
* Parses the HTML to extract semantic element, ARIA attributes, and states.
|
|
17
|
+
* Uses axe-core results to infer what the code supports.
|
|
18
|
+
*/
|
|
19
|
+
export declare function axeResultsToCodeSpec(html: string, axeResults: any): Record<string, any>;
|
|
20
|
+
export declare function registerAccessibilityTools(server: McpServer): void;
|
|
21
|
+
//# sourceMappingURL=accessibility-tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accessibility-tools.d.ts","sourceRoot":"","sources":["../../src/core/accessibility-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA6FpE;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAoGvF;AA+DD,wBAAgB,0BAA0B,CACzC,MAAM,EAAE,SAAS,GACf,IAAI,CAyEN"}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code-side accessibility scanning via axe-core + JSDOM.
|
|
3
|
+
*
|
|
4
|
+
* Delegates all rule logic to axe-core (Deque) — the MCP never owns
|
|
5
|
+
* a rule database. JSDOM provides a lightweight DOM for structural checks
|
|
6
|
+
* (~50 rules: ARIA, semantics, alt text, form labels, headings, landmarks).
|
|
7
|
+
*
|
|
8
|
+
* Visual rules (color contrast, focus-visible) are NOT available via JSDOM —
|
|
9
|
+
* those are handled by the design-side figma_lint_design tool.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { logger } from "./logger.js";
|
|
13
|
+
// Lazy-load axe-core and jsdom to keep them optional
|
|
14
|
+
let axeCore = null;
|
|
15
|
+
let JSDOM = null;
|
|
16
|
+
let depsLoaded = false;
|
|
17
|
+
let depsError = null;
|
|
18
|
+
async function loadDeps() {
|
|
19
|
+
if (depsLoaded)
|
|
20
|
+
return;
|
|
21
|
+
try {
|
|
22
|
+
axeCore = await import("axe-core");
|
|
23
|
+
// axe-core's default export structure
|
|
24
|
+
if (axeCore.default)
|
|
25
|
+
axeCore = axeCore.default;
|
|
26
|
+
const jsdomModule = await import("jsdom");
|
|
27
|
+
JSDOM = jsdomModule.JSDOM;
|
|
28
|
+
depsLoaded = true;
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
depsError = `axe-core or jsdom not installed. Run: npm install axe-core jsdom\n${e.message}`;
|
|
32
|
+
throw new Error(depsError);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Run axe-core against an HTML string using JSDOM.
|
|
37
|
+
*
|
|
38
|
+
* JSDOM limitations: no computed styles, no layout, no visual rendering.
|
|
39
|
+
* This means ~50-60 structural rules work, but visual rules
|
|
40
|
+
* (color-contrast, focus-visible, etc.) will report as "incomplete".
|
|
41
|
+
*/
|
|
42
|
+
async function scanHtmlWithAxe(html, options = {}) {
|
|
43
|
+
await loadDeps();
|
|
44
|
+
// Wrap HTML fragment in a full document if needed
|
|
45
|
+
const fullHtml = html.includes("<html") || html.includes("<!DOCTYPE")
|
|
46
|
+
? html
|
|
47
|
+
: `<!DOCTYPE html><html lang="en"><head><title>Scan</title></head><body>${html}</body></html>`;
|
|
48
|
+
const dom = new JSDOM(fullHtml, {
|
|
49
|
+
runScripts: "dangerously",
|
|
50
|
+
pretendToBeVisual: true,
|
|
51
|
+
url: "http://localhost",
|
|
52
|
+
});
|
|
53
|
+
const { document, window } = dom.window;
|
|
54
|
+
// Inject axe-core into the JSDOM window
|
|
55
|
+
const axeSource = axeCore.source;
|
|
56
|
+
const scriptEl = document.createElement("script");
|
|
57
|
+
scriptEl.textContent = axeSource;
|
|
58
|
+
document.head.appendChild(scriptEl);
|
|
59
|
+
// Configure axe run options
|
|
60
|
+
const runOptions = {};
|
|
61
|
+
if (options.tags && options.tags.length > 0) {
|
|
62
|
+
runOptions.runOnly = { type: "tag", values: options.tags };
|
|
63
|
+
}
|
|
64
|
+
// Disable rules that require visual rendering (always fail/incomplete in JSDOM)
|
|
65
|
+
if (options.disableVisualRules !== false) {
|
|
66
|
+
runOptions.rules = {
|
|
67
|
+
"color-contrast": { enabled: false },
|
|
68
|
+
"color-contrast-enhanced": { enabled: false },
|
|
69
|
+
"link-in-text-block": { enabled: false },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Determine scan context
|
|
73
|
+
const context = options.context || document;
|
|
74
|
+
try {
|
|
75
|
+
const results = await window.axe.run(context, runOptions);
|
|
76
|
+
// Clean up
|
|
77
|
+
dom.window.close();
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
dom.window.close();
|
|
82
|
+
throw new Error(`axe-core scan failed: ${err.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Extract a CodeSpec.accessibility object from HTML + axe-core results.
|
|
87
|
+
* This bridges Phase 3 (code scanning) → Phase 4 (parity comparison).
|
|
88
|
+
*
|
|
89
|
+
* Parses the HTML to extract semantic element, ARIA attributes, and states.
|
|
90
|
+
* Uses axe-core results to infer what the code supports.
|
|
91
|
+
*/
|
|
92
|
+
export function axeResultsToCodeSpec(html, axeResults) {
|
|
93
|
+
const spec = {};
|
|
94
|
+
// Parse HTML to extract attributes (lightweight regex-based, no DOM needed)
|
|
95
|
+
const htmlLower = html.toLowerCase();
|
|
96
|
+
// Semantic element: find the root/first meaningful element
|
|
97
|
+
const rootElementMatch = html.match(/<(button|a|input|select|textarea|details|dialog|nav|main|form|label|fieldset)\b/i);
|
|
98
|
+
if (rootElementMatch) {
|
|
99
|
+
spec.semanticElement = rootElementMatch[1].toLowerCase();
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const firstElementMatch = html.match(/<(\w+)[\s>]/);
|
|
103
|
+
if (firstElementMatch && !["div", "span", "html", "head", "body", "script", "style", "!doctype"].includes(firstElementMatch[1].toLowerCase())) {
|
|
104
|
+
spec.semanticElement = firstElementMatch[1].toLowerCase();
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
spec.semanticElement = "div";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// ARIA role
|
|
111
|
+
const roleMatch = html.match(/role=["']([^"']+)["']/i);
|
|
112
|
+
if (roleMatch) {
|
|
113
|
+
spec.role = roleMatch[1];
|
|
114
|
+
}
|
|
115
|
+
// ARIA label
|
|
116
|
+
const ariaLabelMatch = html.match(/aria-label=["']([^"']+)["']/i);
|
|
117
|
+
if (ariaLabelMatch) {
|
|
118
|
+
spec.ariaLabel = ariaLabelMatch[1];
|
|
119
|
+
}
|
|
120
|
+
// Focus visible: check for :focus-visible or :focus in inline styles/class names,
|
|
121
|
+
// or infer from element type (native interactive elements have default focus)
|
|
122
|
+
const nativeFocusElements = ["button", "a", "input", "select", "textarea"];
|
|
123
|
+
const hasFocusCSS = /focus-visible|:focus\b|outline.*focus|ring.*focus|focus.*ring/i.test(html);
|
|
124
|
+
spec.focusVisible = hasFocusCSS || nativeFocusElements.includes(spec.semanticElement || "");
|
|
125
|
+
// Disabled support: only assert true when we find positive evidence.
|
|
126
|
+
// Absence of disabled/aria-disabled in a single HTML snapshot does NOT mean
|
|
127
|
+
// the component lacks disabled support — it may be in a non-disabled state.
|
|
128
|
+
if (/\bdisabled\b|aria-disabled/i.test(htmlLower)) {
|
|
129
|
+
spec.supportsDisabled = true;
|
|
130
|
+
}
|
|
131
|
+
// (leave undefined when not found — absence ≠ lack of support)
|
|
132
|
+
// Error support: same principle — only assert true on positive evidence.
|
|
133
|
+
// A default-state HTML snippet won't have aria-invalid; that doesn't mean
|
|
134
|
+
// the component can't enter an error state.
|
|
135
|
+
if (/aria-invalid|aria-errormessage|aria-describedby.*error/i.test(htmlLower)) {
|
|
136
|
+
spec.supportsError = true;
|
|
137
|
+
}
|
|
138
|
+
// (leave undefined when not found — scan a different state to confirm)
|
|
139
|
+
// Required: check for required or aria-required attributes
|
|
140
|
+
if (/aria-required=["']true["']|required(?!=)/i.test(html)) {
|
|
141
|
+
spec.ariaRequired = true;
|
|
142
|
+
}
|
|
143
|
+
else if (/aria-required=["']false["']/i.test(html)) {
|
|
144
|
+
spec.ariaRequired = false;
|
|
145
|
+
}
|
|
146
|
+
// Keyboard interactions: infer from element type
|
|
147
|
+
const keyboardInteractions = [];
|
|
148
|
+
if (spec.semanticElement === "button" || spec.role === "button") {
|
|
149
|
+
keyboardInteractions.push("Enter", "Space");
|
|
150
|
+
}
|
|
151
|
+
else if (spec.semanticElement === "a" || spec.role === "link") {
|
|
152
|
+
keyboardInteractions.push("Enter");
|
|
153
|
+
}
|
|
154
|
+
else if (spec.semanticElement === "input" || spec.semanticElement === "textarea") {
|
|
155
|
+
keyboardInteractions.push("Tab (focus)", "Type (input)");
|
|
156
|
+
}
|
|
157
|
+
else if (spec.semanticElement === "select" || spec.role === "listbox") {
|
|
158
|
+
keyboardInteractions.push("Arrow keys", "Enter", "Space");
|
|
159
|
+
}
|
|
160
|
+
else if (spec.role === "checkbox" || spec.role === "switch") {
|
|
161
|
+
keyboardInteractions.push("Space");
|
|
162
|
+
}
|
|
163
|
+
else if (spec.role === "tab") {
|
|
164
|
+
keyboardInteractions.push("Arrow keys");
|
|
165
|
+
}
|
|
166
|
+
// Check HTML for custom keyboard handlers
|
|
167
|
+
if (/onkeydown|onkeyup|onkeypress|@keydown|@keyup|v-on:keydown/i.test(html)) {
|
|
168
|
+
if (!keyboardInteractions.includes("Custom key handler")) {
|
|
169
|
+
keyboardInteractions.push("Custom key handler");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (keyboardInteractions.length > 0) {
|
|
173
|
+
spec.keyboardInteractions = keyboardInteractions;
|
|
174
|
+
}
|
|
175
|
+
// Use axe-core results to refine: if certain violations exist, it tells us what's missing
|
|
176
|
+
if (axeResults?.violations) {
|
|
177
|
+
for (const v of axeResults.violations) {
|
|
178
|
+
// If button-name violation exists, the button has no accessible name
|
|
179
|
+
if (v.id === "button-name") {
|
|
180
|
+
spec.ariaLabel = undefined; // Explicitly missing
|
|
181
|
+
}
|
|
182
|
+
// If label violation exists, input lacks a label
|
|
183
|
+
if (v.id === "label") {
|
|
184
|
+
spec.ariaLabel = undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return spec;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Format axe-core results into our standard lint-like output structure.
|
|
192
|
+
*/
|
|
193
|
+
function formatAxeResults(axeResults) {
|
|
194
|
+
const categories = [];
|
|
195
|
+
const severityMap = {
|
|
196
|
+
critical: "critical",
|
|
197
|
+
serious: "critical",
|
|
198
|
+
moderate: "warning",
|
|
199
|
+
minor: "info",
|
|
200
|
+
};
|
|
201
|
+
// Group violations
|
|
202
|
+
for (const violation of axeResults.violations || []) {
|
|
203
|
+
const severity = severityMap[violation.impact] || "warning";
|
|
204
|
+
const nodes = violation.nodes.map((node) => ({
|
|
205
|
+
html: node.html?.substring(0, 200),
|
|
206
|
+
target: node.target,
|
|
207
|
+
failureSummary: node.failureSummary?.substring(0, 300),
|
|
208
|
+
}));
|
|
209
|
+
categories.push({
|
|
210
|
+
rule: violation.id,
|
|
211
|
+
severity,
|
|
212
|
+
count: violation.nodes.length,
|
|
213
|
+
description: violation.help,
|
|
214
|
+
wcagTags: violation.tags.filter((t) => t.startsWith("wcag") || t.startsWith("best-practice")),
|
|
215
|
+
helpUrl: violation.helpUrl,
|
|
216
|
+
nodes: nodes.slice(0, 10), // Cap at 10 per rule
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// Sort: critical first, then by count
|
|
220
|
+
categories.sort((a, b) => {
|
|
221
|
+
const sevOrder = { critical: 0, warning: 1, info: 2 };
|
|
222
|
+
if (sevOrder[a.severity] !== sevOrder[b.severity]) {
|
|
223
|
+
return sevOrder[a.severity] - sevOrder[b.severity];
|
|
224
|
+
}
|
|
225
|
+
return b.count - a.count;
|
|
226
|
+
});
|
|
227
|
+
// Summary
|
|
228
|
+
const summary = { critical: 0, warning: 0, info: 0, total: 0 };
|
|
229
|
+
for (const cat of categories) {
|
|
230
|
+
summary[cat.severity] += cat.count;
|
|
231
|
+
summary.total += cat.count;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
engine: "axe-core",
|
|
235
|
+
version: axeResults.testEngine?.version || "unknown",
|
|
236
|
+
mode: "jsdom-structural",
|
|
237
|
+
note: "JSDOM mode: structural/semantic checks only. Visual rules (color contrast, focus visibility) are disabled — use figma_lint_design for visual accessibility checks.",
|
|
238
|
+
categories,
|
|
239
|
+
summary,
|
|
240
|
+
passes: axeResults.passes?.length || 0,
|
|
241
|
+
incomplete: axeResults.incomplete?.length || 0,
|
|
242
|
+
inapplicable: axeResults.inapplicable?.length || 0,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
export function registerAccessibilityTools(server) {
|
|
246
|
+
server.tool("figma_scan_code_accessibility", "Scan HTML code for accessibility violations using axe-core (Deque). " +
|
|
247
|
+
"Runs structural/semantic checks via JSDOM: ARIA attributes, roles, labels, alt text, " +
|
|
248
|
+
"form labels, heading order, landmarks, semantic HTML, tabindex, duplicate IDs, lang attribute, and ~50 more rules. " +
|
|
249
|
+
"Visual checks (color contrast, focus visibility) are disabled in this mode — use figma_lint_design for visual a11y on the design side. " +
|
|
250
|
+
"Together, these two tools provide full-spectrum accessibility coverage across design and code. " +
|
|
251
|
+
"Pass component HTML directly or use with figma_check_design_parity for design-to-code a11y comparison. " +
|
|
252
|
+
"No Figma connection required — this is a standalone code analysis tool.", {
|
|
253
|
+
html: z.string().describe("HTML string to scan. Can be a full document or a component fragment (will be wrapped in a valid document)."),
|
|
254
|
+
tags: z.array(z.string()).optional().describe("WCAG tag filter. Examples: ['wcag2a'], ['wcag2aa'], ['wcag21aa'], ['wcag22aa'], ['best-practice']. " +
|
|
255
|
+
"Defaults to all structural rules if omitted."),
|
|
256
|
+
context: z.string().optional().describe("CSS selector to scope the scan to a specific element (e.g., '#my-component', '.card'). Scans entire document if omitted."),
|
|
257
|
+
includePassingRules: z.boolean().optional().describe("If true, includes count of passing and incomplete rules in the response (default: false)."),
|
|
258
|
+
mapToCodeSpec: z.boolean().optional().describe("If true, includes a codeSpec.accessibility object auto-extracted from the HTML + scan results. " +
|
|
259
|
+
"Pass this directly into figma_check_design_parity's codeSpec.accessibility field for automated design-to-code a11y parity checking."),
|
|
260
|
+
}, async ({ html, tags, context, includePassingRules, mapToCodeSpec }) => {
|
|
261
|
+
try {
|
|
262
|
+
const axeResults = await scanHtmlWithAxe(html, {
|
|
263
|
+
tags: tags || undefined,
|
|
264
|
+
context: context || undefined,
|
|
265
|
+
});
|
|
266
|
+
const formatted = formatAxeResults(axeResults);
|
|
267
|
+
// Optionally strip pass/incomplete counts to save tokens
|
|
268
|
+
if (!includePassingRules) {
|
|
269
|
+
delete formatted.passes;
|
|
270
|
+
delete formatted.incomplete;
|
|
271
|
+
delete formatted.inapplicable;
|
|
272
|
+
}
|
|
273
|
+
// Auto-generate CodeSpec.accessibility from HTML + results
|
|
274
|
+
if (mapToCodeSpec) {
|
|
275
|
+
formatted.codeSpecAccessibility = axeResultsToCodeSpec(html, axeResults);
|
|
276
|
+
formatted.codeSpecAccessibility._usage = "Pass this object as codeSpec.accessibility in figma_check_design_parity for automated a11y parity checking.";
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
content: [
|
|
280
|
+
{
|
|
281
|
+
type: "text",
|
|
282
|
+
text: JSON.stringify(formatted, null, 2),
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
const isDepsError = error.message?.includes("not installed");
|
|
289
|
+
logger.error({ error }, "Failed to scan code accessibility");
|
|
290
|
+
return {
|
|
291
|
+
content: [
|
|
292
|
+
{
|
|
293
|
+
type: "text",
|
|
294
|
+
text: JSON.stringify({
|
|
295
|
+
error: error.message,
|
|
296
|
+
hint: isDepsError
|
|
297
|
+
? "Install dependencies: npm install axe-core jsdom"
|
|
298
|
+
: "Check that the HTML is valid. For visual accessibility checks, use figma_lint_design instead.",
|
|
299
|
+
}),
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
isError: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
//# sourceMappingURL=accessibility-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accessibility-tools.js","sourceRoot":"","sources":["../../src/core/accessibility-tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"design-code-tools.d.ts","sourceRoot":"","sources":["../../src/core/design-code-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAK/C,OAAO,KAAK,EAUX,uBAAuB,EACvB,MAAM,wBAAwB,CAAC;AAShC,oDAAoD;AACpD,wBAAgB,cAAc,CAAC,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAU7F;AAED,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAWpD;AAED,8CAA8C;AAC9C,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,GAAE,MAAU,GAAG,OAAO,CAEjF;AAED,qDAAqD;AACrD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzG;AAiED,oEAAoE;AACpE,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAqBpG;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAUxD;AAMD,sEAAsE;AACtE,UAAU,iBAAiB;IAC1B,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,sCAAsC;IACtC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,uEAAuE;IACvE,iBAAiB,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAC/D,0BAA0B;IAC1B,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,yCAAyC;IACzC,eAAe,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,iBAAiB,CA0KhF;AAMD,mDAAmD;AACnD,UAAU,gBAAgB;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5F,OAAO,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9F,UAAU,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjG,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC7C;AAED,uCAAuC;AACvC,UAAU,aAAa;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,gBAAgB,EAAE,CAqBpG;AAoED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,GAAE,MAAU,EAAE,QAAQ,GAAE,MAAU,GAAG,aAAa,EAAE,CAiCzG;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,GAAE,MAAU,EAAE,QAAQ,GAAE,MAAU,GAAG,MAAM,CAqB3F;AAmHD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,GAAG,GAAG,GAAG,CAKhD;AAED,oGAAoG;AACpG,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,uDAAuD;AACvD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;
|
|
1
|
+
{"version":3,"file":"design-code-tools.d.ts","sourceRoot":"","sources":["../../src/core/design-code-tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -990,7 +990,9 @@ function compareAccessibility(node, codeSpec, discrepancies) {
|
|
|
990
990
|
return;
|
|
991
991
|
// Check description/annotations for accessibility hints
|
|
992
992
|
const description = node.descriptionMarkdown || node.description || "";
|
|
993
|
-
const
|
|
993
|
+
const descLower = description.toLowerCase();
|
|
994
|
+
const hasAriaAnnotation = descLower.includes("aria") || descLower.includes("accessibility");
|
|
995
|
+
// ---- 1. ARIA Role Parity ----
|
|
994
996
|
if (ca.role && !hasAriaAnnotation) {
|
|
995
997
|
discrepancies.push({
|
|
996
998
|
category: "accessibility",
|
|
@@ -1002,6 +1004,35 @@ function compareAccessibility(node, codeSpec, discrepancies) {
|
|
|
1002
1004
|
suggestion: "Add accessibility annotations in Figma description",
|
|
1003
1005
|
});
|
|
1004
1006
|
}
|
|
1007
|
+
// ---- 2. Semantic Element vs Component Name ----
|
|
1008
|
+
if (ca.semanticElement) {
|
|
1009
|
+
const nodeName = (node.name || "").toLowerCase();
|
|
1010
|
+
const element = ca.semanticElement.toLowerCase();
|
|
1011
|
+
// Check if interactive component uses correct semantic element
|
|
1012
|
+
const interactivePattern = /button|link|input|checkbox|radio|switch|toggle|tab|select/i;
|
|
1013
|
+
if (interactivePattern.test(nodeName)) {
|
|
1014
|
+
const elementMatchesDesign = (nodeName.includes("button") && (element === "button" || ca.role === "button")) ||
|
|
1015
|
+
(nodeName.includes("link") && (element === "a" || ca.role === "link")) ||
|
|
1016
|
+
(nodeName.includes("input") && (element === "input" || element === "textarea")) ||
|
|
1017
|
+
(nodeName.includes("checkbox") && (element === "input" || ca.role === "checkbox")) ||
|
|
1018
|
+
(nodeName.includes("radio") && (element === "input" || ca.role === "radio")) ||
|
|
1019
|
+
(nodeName.includes("switch") && (ca.role === "switch" || element === "input")) ||
|
|
1020
|
+
(nodeName.includes("select") && (element === "select" || ca.role === "listbox")) ||
|
|
1021
|
+
(nodeName.includes("tab") && (ca.role === "tab" || element === "button"));
|
|
1022
|
+
if (!elementMatchesDesign) {
|
|
1023
|
+
discrepancies.push({
|
|
1024
|
+
category: "accessibility",
|
|
1025
|
+
property: "semanticElement",
|
|
1026
|
+
severity: "major",
|
|
1027
|
+
designValue: nodeName,
|
|
1028
|
+
codeValue: `<${element}>${ca.role ? ` role="${ca.role}"` : ""}`,
|
|
1029
|
+
message: `Design component "${node.name}" may not match code element <${element}>`,
|
|
1030
|
+
suggestion: `Verify that <${element}> is the correct semantic element for a component named "${node.name}". Use native HTML elements over ARIA roles where possible.`,
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
// ---- 3. Contrast Ratio ----
|
|
1005
1036
|
if (ca.contrastRatio !== undefined && ca.contrastRatio < 4.5) {
|
|
1006
1037
|
discrepancies.push({
|
|
1007
1038
|
category: "accessibility",
|
|
@@ -1013,6 +1044,129 @@ function compareAccessibility(node, codeSpec, discrepancies) {
|
|
|
1013
1044
|
suggestion: "Increase contrast ratio to at least 4.5:1",
|
|
1014
1045
|
});
|
|
1015
1046
|
}
|
|
1047
|
+
// ---- 4. Focus Indicator Parity ----
|
|
1048
|
+
// Check if design has a focus variant but code doesn't implement focus-visible
|
|
1049
|
+
const variants = node.children || [];
|
|
1050
|
+
const hasFocusVariant = variants.some((v) => /focus|focused/i.test(v.name || ""));
|
|
1051
|
+
if (hasFocusVariant && ca.focusVisible === false) {
|
|
1052
|
+
discrepancies.push({
|
|
1053
|
+
category: "accessibility",
|
|
1054
|
+
property: "focusVisible",
|
|
1055
|
+
severity: "critical",
|
|
1056
|
+
designValue: "focus variant exists",
|
|
1057
|
+
codeValue: "focusVisible: false",
|
|
1058
|
+
message: "Design has a focus variant but code does not implement :focus-visible styles",
|
|
1059
|
+
suggestion: "Add :focus-visible CSS with a visible focus ring matching the design's focus variant (WCAG 2.4.7)",
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
else if (!hasFocusVariant && ca.focusVisible === true) {
|
|
1063
|
+
discrepancies.push({
|
|
1064
|
+
category: "accessibility",
|
|
1065
|
+
property: "focusVisible",
|
|
1066
|
+
severity: "minor",
|
|
1067
|
+
designValue: "no focus variant",
|
|
1068
|
+
codeValue: "focusVisible: true",
|
|
1069
|
+
message: "Code implements :focus-visible but design has no focus variant to specify the visual treatment",
|
|
1070
|
+
suggestion: "Add a focus/focused variant in Figma to document the intended focus indicator design",
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
// ---- 5. Disabled State Parity ----
|
|
1074
|
+
const hasDisabledVariant = variants.some((v) => /disabled|inactive/i.test(v.name || ""));
|
|
1075
|
+
if (hasDisabledVariant && ca.supportsDisabled === false) {
|
|
1076
|
+
discrepancies.push({
|
|
1077
|
+
category: "accessibility",
|
|
1078
|
+
property: "disabled",
|
|
1079
|
+
severity: "major",
|
|
1080
|
+
designValue: "disabled variant exists",
|
|
1081
|
+
codeValue: "supportsDisabled: false",
|
|
1082
|
+
message: "Design has a disabled variant but code does not support disabled/aria-disabled state",
|
|
1083
|
+
suggestion: "Implement disabled or aria-disabled attribute support in the component",
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
else if (!hasDisabledVariant && ca.supportsDisabled === true) {
|
|
1087
|
+
discrepancies.push({
|
|
1088
|
+
category: "accessibility",
|
|
1089
|
+
property: "disabled",
|
|
1090
|
+
severity: "minor",
|
|
1091
|
+
designValue: "no disabled variant",
|
|
1092
|
+
codeValue: "supportsDisabled: true",
|
|
1093
|
+
message: "Code supports disabled state but design has no disabled variant",
|
|
1094
|
+
suggestion: "Add a disabled variant in Figma showing the visual treatment for disabled state",
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
// ---- 6. Error State Parity ----
|
|
1098
|
+
const hasErrorVariant = variants.some((v) => /error|invalid|danger/i.test(v.name || ""));
|
|
1099
|
+
if (hasErrorVariant && ca.supportsError === false) {
|
|
1100
|
+
discrepancies.push({
|
|
1101
|
+
category: "accessibility",
|
|
1102
|
+
property: "errorState",
|
|
1103
|
+
severity: "major",
|
|
1104
|
+
designValue: "error variant exists",
|
|
1105
|
+
codeValue: "supportsError: false",
|
|
1106
|
+
message: "Design has an error variant but code does not support aria-invalid or error messaging",
|
|
1107
|
+
suggestion: "Implement aria-invalid attribute and associated error message (aria-describedby) in the component",
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
// ---- 7. Required Field Parity ----
|
|
1111
|
+
if (ca.ariaRequired !== undefined) {
|
|
1112
|
+
const hasRequiredVariant = variants.some((v) => /required/i.test(v.name || ""));
|
|
1113
|
+
const hasRequiredInDescription = descLower.includes("required");
|
|
1114
|
+
if (ca.ariaRequired && !hasRequiredVariant && !hasRequiredInDescription) {
|
|
1115
|
+
discrepancies.push({
|
|
1116
|
+
category: "accessibility",
|
|
1117
|
+
property: "required",
|
|
1118
|
+
severity: "minor",
|
|
1119
|
+
designValue: "no required indicator",
|
|
1120
|
+
codeValue: "ariaRequired: true",
|
|
1121
|
+
message: "Code marks field as required but design has no visual required indicator",
|
|
1122
|
+
suggestion: "Add a required indicator (asterisk, label text) in the design and/or a required variant",
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
// ---- 8. Target Size Parity ----
|
|
1127
|
+
if (ca.renderedSize) {
|
|
1128
|
+
const [codeWidth, codeHeight] = ca.renderedSize;
|
|
1129
|
+
const designWidth = node.absoluteBoundingBox?.width || node.size?.x;
|
|
1130
|
+
const designHeight = node.absoluteBoundingBox?.height || node.size?.y;
|
|
1131
|
+
if (designWidth && designHeight) {
|
|
1132
|
+
// Check if code size is significantly smaller than design (>20% reduction)
|
|
1133
|
+
if (codeWidth < designWidth * 0.8 || codeHeight < designHeight * 0.8) {
|
|
1134
|
+
discrepancies.push({
|
|
1135
|
+
category: "accessibility",
|
|
1136
|
+
property: "targetSize",
|
|
1137
|
+
severity: "major",
|
|
1138
|
+
designValue: `${Math.round(designWidth)}x${Math.round(designHeight)}`,
|
|
1139
|
+
codeValue: `${codeWidth}x${codeHeight}`,
|
|
1140
|
+
message: `Code renders significantly smaller (${codeWidth}x${codeHeight}px) than design (${Math.round(designWidth)}x${Math.round(designHeight)}px)`,
|
|
1141
|
+
suggestion: "Ensure rendered component meets the design's touch target size. Check CSS min-width/min-height.",
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
// Check WCAG 2.5.8 minimum (24x24)
|
|
1145
|
+
if (codeWidth < 24 || codeHeight < 24) {
|
|
1146
|
+
discrepancies.push({
|
|
1147
|
+
category: "accessibility",
|
|
1148
|
+
property: "targetSize",
|
|
1149
|
+
severity: "critical",
|
|
1150
|
+
designValue: `${Math.round(designWidth)}x${Math.round(designHeight)}`,
|
|
1151
|
+
codeValue: `${codeWidth}x${codeHeight}`,
|
|
1152
|
+
message: `Code renders below WCAG 2.5.8 minimum (24x24px): ${codeWidth}x${codeHeight}px`,
|
|
1153
|
+
suggestion: "Increase touch target size to at least 24x24px",
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
// ---- 9. Keyboard Interactions ----
|
|
1159
|
+
if (ca.keyboardInteractions && ca.keyboardInteractions.length > 0 && !descLower.includes("keyboard")) {
|
|
1160
|
+
discrepancies.push({
|
|
1161
|
+
category: "accessibility",
|
|
1162
|
+
property: "keyboardInteractions",
|
|
1163
|
+
severity: "info",
|
|
1164
|
+
designValue: null,
|
|
1165
|
+
codeValue: ca.keyboardInteractions.join(", "),
|
|
1166
|
+
message: `Code defines keyboard interactions (${ca.keyboardInteractions.join(", ")}) but design has no keyboard documentation`,
|
|
1167
|
+
suggestion: "Document keyboard interactions in the Figma component description for developer handoff",
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1016
1170
|
}
|
|
1017
1171
|
function compareNaming(node, codeSpec, discrepancies) {
|
|
1018
1172
|
const cm = codeSpec.metadata;
|
|
@@ -2023,7 +2177,11 @@ const codeSpecSchema = z.object({
|
|
|
2023
2177
|
keyboardInteractions: z.array(z.string()).optional(),
|
|
2024
2178
|
contrastRatio: z.number().optional(),
|
|
2025
2179
|
focusVisible: z.boolean().optional(),
|
|
2026
|
-
|
|
2180
|
+
semanticElement: z.string().optional().describe("Semantic HTML element (e.g., 'button', 'a', 'input')"),
|
|
2181
|
+
supportsDisabled: z.boolean().optional().describe("Whether code supports disabled/aria-disabled state"),
|
|
2182
|
+
supportsError: z.boolean().optional().describe("Whether code supports aria-invalid/error state"),
|
|
2183
|
+
renderedSize: z.tuple([z.number(), z.number()]).optional().describe("Rendered size [width, height] in px"),
|
|
2184
|
+
}).optional().describe("Accessibility properties from code. Tip: use figma_scan_code_accessibility with mapToCodeSpec:true to auto-generate this from component HTML."),
|
|
2027
2185
|
metadata: z.object({
|
|
2028
2186
|
name: z.string().optional(),
|
|
2029
2187
|
description: z.string().optional(),
|