@atezer/figma-mcp-bridge 1.9.3 → 1.9.5
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 +93 -0
- package/README.md +29 -0
- package/dist/core/discovery-counter.d.ts +54 -0
- package/dist/core/discovery-counter.d.ts.map +1 -0
- package/dist/core/discovery-counter.js +140 -0
- package/dist/core/discovery-counter.js.map +1 -0
- package/dist/core/plugin-bridge-connector.d.ts +16 -0
- package/dist/core/plugin-bridge-connector.d.ts.map +1 -1
- package/dist/core/plugin-bridge-connector.js +13 -0
- package/dist/core/plugin-bridge-connector.js.map +1 -1
- package/dist/local-plugin-only.d.ts.map +1 -1
- package/dist/local-plugin-only.js +208 -24
- package/dist/local-plugin-only.js.map +1 -1
- package/f-mcp-plugin/code.js +370 -34
- package/f-mcp-plugin/ui.html +1 -1
- package/package.json +1 -1
- package/skills/figma-canvas-ops/SKILL.md +68 -0
- package/skills/fmcp-intent-router/SKILL.md +9 -0
- package/skills/fmcp-screen-orchestrator/SKILL.md +29 -0
- package/skills/fmcp-screen-recipes/SKILL.md +31 -5
package/f-mcp-plugin/code.js
CHANGED
|
@@ -6,7 +6,97 @@
|
|
|
6
6
|
|
|
7
7
|
// v1.8.0+: Plugin version reported in WebSocket "ready" handshake.
|
|
8
8
|
// Keep in sync with package.json and src/core/version.ts.
|
|
9
|
-
var FMCP_PLUGIN_VERSION = '1.9.
|
|
9
|
+
var FMCP_PLUGIN_VERSION = '1.9.5';
|
|
10
|
+
|
|
11
|
+
// v1.9.5: Metadata summary builder for returnMode='summary' (screenshotsuz)
|
|
12
|
+
async function buildNodeSummary(node, maxDepth) {
|
|
13
|
+
var colorUsage = {}; // role → count
|
|
14
|
+
var textRoleCount = {}; // styleId → { count, samples }
|
|
15
|
+
var unboundHexCount = 0;
|
|
16
|
+
var totalNodes = 0;
|
|
17
|
+
|
|
18
|
+
function walk(n, depth) {
|
|
19
|
+
totalNodes++;
|
|
20
|
+
// Fill bound token tracking
|
|
21
|
+
if (Array.isArray(n.fills)) {
|
|
22
|
+
for (var fi = 0; fi < n.fills.length; fi++) {
|
|
23
|
+
var fl = n.fills[fi];
|
|
24
|
+
if (fl && fl.visible !== false && fl.type === 'SOLID') {
|
|
25
|
+
var fbv = n.boundVariables && n.boundVariables.fills;
|
|
26
|
+
var bound = fbv && (Array.isArray(fbv) ? fbv[fi] : true);
|
|
27
|
+
if (bound) {
|
|
28
|
+
var role = (Array.isArray(fbv) ? fbv[fi] : fbv).id || 'unknown';
|
|
29
|
+
colorUsage[role] = (colorUsage[role] || 0) + 1;
|
|
30
|
+
} else {
|
|
31
|
+
unboundHexCount++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (n.type === 'TEXT') {
|
|
37
|
+
var styleId = n.textStyleId || 'inline';
|
|
38
|
+
if (!textRoleCount[styleId]) textRoleCount[styleId] = { count: 0, samples: [] };
|
|
39
|
+
textRoleCount[styleId].count++;
|
|
40
|
+
if (textRoleCount[styleId].samples.length < 3) {
|
|
41
|
+
textRoleCount[styleId].samples.push(String(n.characters || '').slice(0, 30));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (n.children && depth < 3) { // deep walk but capped
|
|
45
|
+
for (var ci = 0; ci < n.children.length; ci++) walk(n.children[ci], depth + 1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
walk(node, 0);
|
|
50
|
+
|
|
51
|
+
function collectSections(n, depth) {
|
|
52
|
+
if (depth > maxDepth) return null;
|
|
53
|
+
var s = {
|
|
54
|
+
name: n.name,
|
|
55
|
+
type: n.type,
|
|
56
|
+
size: n.width && n.height ? Math.round(n.width) + 'x' + Math.round(n.height) : null,
|
|
57
|
+
childrenCount: n.children ? n.children.length : 0,
|
|
58
|
+
};
|
|
59
|
+
if (n.children && n.children.length > 0 && depth < maxDepth) {
|
|
60
|
+
var items = [];
|
|
61
|
+
for (var i = 0; i < n.children.length && i < 20; i++) {
|
|
62
|
+
items.push(collectSections(n.children[i], depth + 1));
|
|
63
|
+
}
|
|
64
|
+
s.children = items;
|
|
65
|
+
}
|
|
66
|
+
return s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Resolve variable names (best-effort)
|
|
70
|
+
var dominantColors = [];
|
|
71
|
+
var colorEntries = Object.keys(colorUsage).map(function (k) { return { role: k, usage: colorUsage[k] }; });
|
|
72
|
+
colorEntries.sort(function (a, b) { return b.usage - a.usage; });
|
|
73
|
+
for (var ci2 = 0; ci2 < Math.min(colorEntries.length, 5); ci2++) {
|
|
74
|
+
dominantColors.push({ variableId: colorEntries[ci2].role, usageCount: colorEntries[ci2].usage, bound: true });
|
|
75
|
+
}
|
|
76
|
+
if (unboundHexCount > 0) {
|
|
77
|
+
dominantColors.push({ variableId: '<hardcoded>', usageCount: unboundHexCount, bound: false });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
var textRoles = [];
|
|
81
|
+
var trKeys = Object.keys(textRoleCount);
|
|
82
|
+
for (var tk = 0; tk < trKeys.length; tk++) {
|
|
83
|
+
textRoles.push({ styleId: trKeys[tk], count: textRoleCount[trKeys[tk]].count, samples: textRoleCount[trKeys[tk]].samples });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
nodeId: node.id,
|
|
88
|
+
name: node.name,
|
|
89
|
+
type: node.type,
|
|
90
|
+
dimensions: { width: Math.round(node.width || 0), height: Math.round(node.height || 0) },
|
|
91
|
+
layoutMode: node.layoutMode || 'NONE',
|
|
92
|
+
childrenCount: node.children ? node.children.length : 0,
|
|
93
|
+
totalNodes: totalNodes,
|
|
94
|
+
sections: collectSections(node, 0),
|
|
95
|
+
dominantColors: dominantColors,
|
|
96
|
+
textRoles: textRoles.slice(0, 10),
|
|
97
|
+
hint: "v1.9.5 summary: Metadata ozeti, gorsel yok. Layout karari icin yeter. Pixel dogrulama gerekirse returnMode='file' veya 'regions'.",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
10
100
|
|
|
11
101
|
// Console log buffer for figma_get_console_logs (no CDP)
|
|
12
102
|
var __consoleLogBuffer = [];
|
|
@@ -2391,6 +2481,9 @@ figma.ui.onmessage = async (msg) => {
|
|
|
2391
2481
|
// Pass 2: Promise.all on getMainComponentAsync for all instances in parallel
|
|
2392
2482
|
// This cuts 100+ node validation from ~60s to ~5s.
|
|
2393
2483
|
|
|
2484
|
+
// v1.9.4: detailed param — if true, collect hardcoded samples + overflow + primitive fallbacks.
|
|
2485
|
+
var detailed = msg.detailed === true;
|
|
2486
|
+
|
|
2394
2487
|
// Iterative tree walk (stack-safe for deep trees up to 10,000+ nodes)
|
|
2395
2488
|
var metrics = {
|
|
2396
2489
|
totalNodes: 0,
|
|
@@ -2400,11 +2493,33 @@ figma.ui.onmessage = async (msg) => {
|
|
|
2400
2493
|
libraryInstanceNodes: 0,
|
|
2401
2494
|
stylableNodes: 0,
|
|
2402
2495
|
nodesWithBindings: 0,
|
|
2496
|
+
// v1.9.4: extra coverage metrics
|
|
2497
|
+
boundFills: 0,
|
|
2498
|
+
unboundSolidFills: 0,
|
|
2499
|
+
paddingsTotal: 0,
|
|
2500
|
+
paddingsBound: 0,
|
|
2501
|
+
itemSpacingTotal: 0,
|
|
2502
|
+
itemSpacingBound: 0,
|
|
2503
|
+
radiusTotal: 0,
|
|
2504
|
+
radiusBound: 0,
|
|
2505
|
+
textsTotal: 0,
|
|
2506
|
+
textsWithStyle: 0,
|
|
2507
|
+
textsHardcodedFontSize: 0,
|
|
2508
|
+
textColorsTotal: 0,
|
|
2509
|
+
textColorsBound: 0,
|
|
2510
|
+
strokesTotal: 0,
|
|
2511
|
+
strokesBound: 0,
|
|
2403
2512
|
};
|
|
2404
2513
|
var violations = [];
|
|
2405
2514
|
var maxViolations = 20; // cap to keep response small
|
|
2406
2515
|
var instanceNodesList = []; // v1.8.2: collect for batch resolve
|
|
2407
2516
|
|
|
2517
|
+
// v1.9.4: detailed mode samples (capped small)
|
|
2518
|
+
var hardcodedHexSamples = [];
|
|
2519
|
+
var hardcodedFontSizeSamples = [];
|
|
2520
|
+
var primitiveFallbacks = [];
|
|
2521
|
+
var maxSamples = 8;
|
|
2522
|
+
|
|
2408
2523
|
var walkStack = [rootNode];
|
|
2409
2524
|
var visited = 0;
|
|
2410
2525
|
var maxVisit = 5000; // safety cap
|
|
@@ -2479,6 +2594,95 @@ figma.ui.onmessage = async (msg) => {
|
|
|
2479
2594
|
}
|
|
2480
2595
|
}
|
|
2481
2596
|
|
|
2597
|
+
// v1.9.4: Granular bind coverage — fills/strokes/paddings/radius/itemSpacing/text
|
|
2598
|
+
// Separate from stylableNodes/nodesWithBindings (which is coarse grained).
|
|
2599
|
+
try {
|
|
2600
|
+
if (Array.isArray(node.fills) && node.type !== 'INSTANCE') {
|
|
2601
|
+
for (var fi = 0; fi < node.fills.length; fi++) {
|
|
2602
|
+
var fl = node.fills[fi];
|
|
2603
|
+
if (fl && fl.visible !== false && fl.type === 'SOLID') {
|
|
2604
|
+
var fbv = node.boundVariables && node.boundVariables.fills;
|
|
2605
|
+
var fillBound = !!(fbv && (Array.isArray(fbv) ? fbv[fi] : true));
|
|
2606
|
+
if (fillBound) metrics.boundFills++;
|
|
2607
|
+
else {
|
|
2608
|
+
metrics.unboundSolidFills++;
|
|
2609
|
+
if (detailed && hardcodedHexSamples.length < maxSamples) {
|
|
2610
|
+
var c = fl.color;
|
|
2611
|
+
var hex = '#' + [c.r, c.g, c.b].map(function (v) { return Math.round(v * 255).toString(16).padStart(2, '0'); }).join('');
|
|
2612
|
+
hardcodedHexSamples.push({ node: node.name, type: node.type, hex: hex });
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
if (Array.isArray(node.strokes) && node.type !== 'INSTANCE') {
|
|
2619
|
+
for (var si = 0; si < node.strokes.length; si++) {
|
|
2620
|
+
var st = node.strokes[si];
|
|
2621
|
+
if (st && st.type === 'SOLID') {
|
|
2622
|
+
metrics.strokesTotal++;
|
|
2623
|
+
var sbv = node.boundVariables && node.boundVariables.strokes;
|
|
2624
|
+
if (sbv && (Array.isArray(sbv) ? sbv[si] : true)) metrics.strokesBound++;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
} catch (e) { /* skip */ }
|
|
2629
|
+
|
|
2630
|
+
if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE') {
|
|
2631
|
+
try {
|
|
2632
|
+
var padProps = ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'];
|
|
2633
|
+
for (var pi = 0; pi < padProps.length; pi++) {
|
|
2634
|
+
var pp = padProps[pi];
|
|
2635
|
+
if (typeof node[pp] === 'number' && node[pp] > 0) {
|
|
2636
|
+
metrics.paddingsTotal++;
|
|
2637
|
+
if (node.boundVariables && node.boundVariables[pp]) metrics.paddingsBound++;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
if (typeof node.itemSpacing === 'number' && node.itemSpacing > 0) {
|
|
2641
|
+
metrics.itemSpacingTotal++;
|
|
2642
|
+
if (node.boundVariables && node.boundVariables.itemSpacing) metrics.itemSpacingBound++;
|
|
2643
|
+
}
|
|
2644
|
+
var radProps = ['cornerRadius', 'topLeftRadius', 'topRightRadius', 'bottomLeftRadius', 'bottomRightRadius'];
|
|
2645
|
+
for (var ri = 0; ri < radProps.length; ri++) {
|
|
2646
|
+
var rp = radProps[ri];
|
|
2647
|
+
if (typeof node[rp] === 'number' && node[rp] > 0) {
|
|
2648
|
+
metrics.radiusTotal++;
|
|
2649
|
+
if (node.boundVariables && node.boundVariables[rp]) metrics.radiusBound++;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
// v1.9.4 detailed: flag primitive frames that *should* have been components
|
|
2653
|
+
if (detailed && node.type === 'FRAME' && node !== rootNode && primitiveFallbacks.length < maxSamples && node.children && node.children.length >= 2) {
|
|
2654
|
+
var onlyPrimitives = true;
|
|
2655
|
+
for (var cpi = 0; cpi < node.children.length; cpi++) {
|
|
2656
|
+
if (node.children[cpi].type === 'INSTANCE') { onlyPrimitives = false; break; }
|
|
2657
|
+
}
|
|
2658
|
+
if (onlyPrimitives) primitiveFallbacks.push({ name: node.name, childCount: node.children.length });
|
|
2659
|
+
}
|
|
2660
|
+
} catch (e) { /* skip */ }
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
if (node.type === 'TEXT') {
|
|
2664
|
+
try {
|
|
2665
|
+
metrics.textsTotal++;
|
|
2666
|
+
if (node.textStyleId && typeof node.textStyleId === 'string' && node.textStyleId !== '') {
|
|
2667
|
+
metrics.textsWithStyle++;
|
|
2668
|
+
} else {
|
|
2669
|
+
metrics.textsHardcodedFontSize++;
|
|
2670
|
+
if (detailed && hardcodedFontSizeSamples.length < maxSamples) {
|
|
2671
|
+
hardcodedFontSizeSamples.push({
|
|
2672
|
+
text: String(node.characters || '').slice(0, 40),
|
|
2673
|
+
fontSize: node.fontSize,
|
|
2674
|
+
fontName: node.fontName,
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
if (Array.isArray(node.fills) && node.fills.length > 0) {
|
|
2679
|
+
metrics.textColorsTotal++;
|
|
2680
|
+
var tbv = node.boundVariables && node.boundVariables.fills;
|
|
2681
|
+
if (tbv && (Array.isArray(tbv) ? tbv[0] : true)) metrics.textColorsBound++;
|
|
2682
|
+
}
|
|
2683
|
+
} catch (e) { /* skip */ }
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2482
2686
|
// Push children
|
|
2483
2687
|
if (node.children) {
|
|
2484
2688
|
for (var ci = 0; ci < node.children.length; ci++) {
|
|
@@ -2543,7 +2747,50 @@ figma.ui.onmessage = async (msg) => {
|
|
|
2543
2747
|
: 'Score ' + score + '/100 is below minimum ' + minScore + '. Consider deleting this screen and rebuilding using ' +
|
|
2544
2748
|
'DS components (figma_instantiate_component) and token bindings (figma_bind_variable).';
|
|
2545
2749
|
|
|
2546
|
-
|
|
2750
|
+
// v1.9.4: granular bind coverage percentages
|
|
2751
|
+
function pct(b, t) { return t > 0 ? Math.round((b / t) * 100) : 100; }
|
|
2752
|
+
var fillsTotal = metrics.boundFills + metrics.unboundSolidFills;
|
|
2753
|
+
var coverage = {
|
|
2754
|
+
fills: { bound: metrics.boundFills, total: fillsTotal, pct: pct(metrics.boundFills, fillsTotal) },
|
|
2755
|
+
paddings: { bound: metrics.paddingsBound, total: metrics.paddingsTotal, pct: pct(metrics.paddingsBound, metrics.paddingsTotal) },
|
|
2756
|
+
itemSpacing: { bound: metrics.itemSpacingBound, total: metrics.itemSpacingTotal, pct: pct(metrics.itemSpacingBound, metrics.itemSpacingTotal) },
|
|
2757
|
+
radius: { bound: metrics.radiusBound, total: metrics.radiusTotal, pct: pct(metrics.radiusBound, metrics.radiusTotal) },
|
|
2758
|
+
textStyle: { withStyle: metrics.textsWithStyle, hardcoded: metrics.textsHardcodedFontSize, total: metrics.textsTotal, pct: pct(metrics.textsWithStyle, metrics.textsTotal) },
|
|
2759
|
+
textColor: { bound: metrics.textColorsBound, total: metrics.textColorsTotal, pct: pct(metrics.textColorsBound, metrics.textColorsTotal) },
|
|
2760
|
+
strokes: { bound: metrics.strokesBound, total: metrics.strokesTotal, pct: pct(metrics.strokesBound, metrics.strokesTotal) },
|
|
2761
|
+
};
|
|
2762
|
+
|
|
2763
|
+
// v1.9.4: overflow check (root level auto-layout containers)
|
|
2764
|
+
var overflow = null;
|
|
2765
|
+
try {
|
|
2766
|
+
if (rootNode.layoutMode === 'VERTICAL' && rootNode.children) {
|
|
2767
|
+
var contentH = 0;
|
|
2768
|
+
for (var chi = 0; chi < rootNode.children.length; chi++) contentH += (rootNode.children[chi].height || 0);
|
|
2769
|
+
if (rootNode.itemSpacing) contentH += rootNode.itemSpacing * Math.max(0, rootNode.children.length - 1);
|
|
2770
|
+
contentH += (rootNode.paddingTop || 0) + (rootNode.paddingBottom || 0);
|
|
2771
|
+
overflow = {
|
|
2772
|
+
axis: 'VERTICAL',
|
|
2773
|
+
frameSize: Math.round(rootNode.height || 0),
|
|
2774
|
+
contentSize: Math.round(contentH),
|
|
2775
|
+
overflowPx: Math.round(contentH - (rootNode.height || 0)),
|
|
2776
|
+
clipsContent: !!rootNode.clipsContent,
|
|
2777
|
+
};
|
|
2778
|
+
} else if (rootNode.layoutMode === 'HORIZONTAL' && rootNode.children) {
|
|
2779
|
+
var contentW = 0;
|
|
2780
|
+
for (var cwi = 0; cwi < rootNode.children.length; cwi++) contentW += (rootNode.children[cwi].width || 0);
|
|
2781
|
+
if (rootNode.itemSpacing) contentW += rootNode.itemSpacing * Math.max(0, rootNode.children.length - 1);
|
|
2782
|
+
contentW += (rootNode.paddingLeft || 0) + (rootNode.paddingRight || 0);
|
|
2783
|
+
overflow = {
|
|
2784
|
+
axis: 'HORIZONTAL',
|
|
2785
|
+
frameSize: Math.round(rootNode.width || 0),
|
|
2786
|
+
contentSize: Math.round(contentW),
|
|
2787
|
+
overflowPx: Math.round(contentW - (rootNode.width || 0)),
|
|
2788
|
+
clipsContent: !!rootNode.clipsContent,
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
} catch (e) { overflow = null; }
|
|
2792
|
+
|
|
2793
|
+
var response = {
|
|
2547
2794
|
type: 'VALIDATE_SCREEN_RESULT',
|
|
2548
2795
|
requestId: msg.requestId,
|
|
2549
2796
|
success: true,
|
|
@@ -2561,12 +2808,25 @@ figma.ui.onmessage = async (msg) => {
|
|
|
2561
2808
|
nodesWithBindings: metrics.nodesWithBindings,
|
|
2562
2809
|
totalStylable: metrics.stylableNodes,
|
|
2563
2810
|
},
|
|
2811
|
+
coverage: coverage, // v1.9.4: granular coverage
|
|
2812
|
+
overflow: overflow, // v1.9.4: root auto-layout overflow
|
|
2564
2813
|
violations: violations,
|
|
2565
2814
|
violationCount: violations.length,
|
|
2566
2815
|
violationCapped: violations.length >= maxViolations,
|
|
2567
2816
|
totalNodesWalked: visited,
|
|
2568
2817
|
recommendation: recommendation,
|
|
2569
|
-
}
|
|
2818
|
+
};
|
|
2819
|
+
|
|
2820
|
+
// v1.9.4: detailed mode — hardcoded samples + primitive fallbacks
|
|
2821
|
+
if (detailed) {
|
|
2822
|
+
response.samples = {
|
|
2823
|
+
hardcodedHex: hardcodedHexSamples,
|
|
2824
|
+
hardcodedFontSize: hardcodedFontSizeSamples,
|
|
2825
|
+
primitiveFrames: primitiveFallbacks,
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
figma.ui.postMessage(response);
|
|
2570
2830
|
} catch (error) {
|
|
2571
2831
|
var errorMsg = error && error.message ? error.message : String(error);
|
|
2572
2832
|
console.error('🌉 [F-MCP] Validate screen error:', errorMsg);
|
|
@@ -2815,64 +3075,140 @@ figma.ui.onmessage = async (msg) => {
|
|
|
2815
3075
|
// ============================================================================
|
|
2816
3076
|
else if (msg.type === 'CAPTURE_SCREENSHOT') {
|
|
2817
3077
|
try {
|
|
2818
|
-
console.log('🌉 [F-MCP ATezer Bridge] Capturing screenshot for node:', msg.nodeId);
|
|
3078
|
+
console.log('🌉 [F-MCP ATezer Bridge v1.9.5] Capturing screenshot for node:', msg.nodeId, 'returnMode:', msg.returnMode);
|
|
2819
3079
|
|
|
2820
3080
|
var node = msg.nodeId ? await figma.getNodeByIdAsync(msg.nodeId) : figma.currentPage;
|
|
2821
3081
|
if (!node) {
|
|
2822
3082
|
throw new Error('Node not found: ' + msg.nodeId);
|
|
2823
3083
|
}
|
|
2824
3084
|
|
|
2825
|
-
//
|
|
2826
|
-
if (!('exportAsync' in node)) {
|
|
2827
|
-
throw new Error('Node type ' + node.type + ' does not support export');
|
|
2828
|
-
}
|
|
2829
|
-
|
|
2830
|
-
// v1.8.0: Default JPG@1x q70 for context safety. Plugin reads msg.format,
|
|
2831
|
-
// msg.scale, msg.jpegQuality from the request payload (passed through
|
|
2832
|
-
// captureScreenshot connector → bridge → here).
|
|
3085
|
+
// v1.8.0: Default JPG@1x q70 for context safety.
|
|
2833
3086
|
var format = (msg.format || 'JPG').toUpperCase();
|
|
2834
3087
|
var scale = msg.scale != null ? msg.scale : 1;
|
|
2835
3088
|
var jpegQuality = msg.jpegQuality != null ? msg.jpegQuality : 70;
|
|
3089
|
+
// v1.9.5: returnMode (default 'file' which server translates to base64 + save)
|
|
3090
|
+
var returnMode = msg.returnMode || 'base64'; // plugin default stays base64; server wraps to file
|
|
3091
|
+
var regionStrategy = msg.regionStrategy || 'children';
|
|
3092
|
+
var maxRegions = msg.maxRegions != null ? msg.maxRegions : 8;
|
|
2836
3093
|
|
|
2837
|
-
|
|
2838
|
-
format: format,
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
// JPG quality (Figma Plugin API supports 'quality' for ExportSettingsImage)
|
|
2842
|
-
if (format === 'JPG') {
|
|
2843
|
-
exportSettings.quality = jpegQuality;
|
|
3094
|
+
function buildExportSettings() {
|
|
3095
|
+
var s = { format: format, constraint: { type: 'SCALE', value: scale } };
|
|
3096
|
+
if (format === 'JPG') s.quality = jpegQuality;
|
|
3097
|
+
return s;
|
|
2844
3098
|
}
|
|
2845
3099
|
|
|
2846
|
-
//
|
|
2847
|
-
|
|
3100
|
+
// ---------- v1.9.5 MODE: summary ----------
|
|
3101
|
+
// No export. Just metadata.
|
|
3102
|
+
if (returnMode === 'summary') {
|
|
3103
|
+
var rootSummary = await buildNodeSummary(node, /*depth*/ 2);
|
|
3104
|
+
figma.ui.postMessage({
|
|
3105
|
+
type: 'CAPTURE_SCREENSHOT_RESULT',
|
|
3106
|
+
requestId: msg.requestId,
|
|
3107
|
+
success: true,
|
|
3108
|
+
mode: 'summary',
|
|
3109
|
+
summary: rootSummary,
|
|
3110
|
+
});
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
2848
3113
|
|
|
2849
|
-
//
|
|
2850
|
-
|
|
3114
|
+
// ---------- v1.9.5 MODE: regions (children strategy) ----------
|
|
3115
|
+
if (returnMode === 'regions' && regionStrategy === 'children') {
|
|
3116
|
+
if (!node.children || node.children.length === 0) {
|
|
3117
|
+
figma.ui.postMessage({
|
|
3118
|
+
type: 'CAPTURE_SCREENSHOT_RESULT',
|
|
3119
|
+
requestId: msg.requestId,
|
|
3120
|
+
success: false,
|
|
3121
|
+
error: "Node has no children for regions mode. Use 'file' or 'summary' instead.",
|
|
3122
|
+
});
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
var regions = [];
|
|
3126
|
+
var children = node.children.slice(0, maxRegions);
|
|
3127
|
+
for (var ri = 0; ri < children.length; ri++) {
|
|
3128
|
+
var child = children[ri];
|
|
3129
|
+
if (!('exportAsync' in child)) continue;
|
|
3130
|
+
try {
|
|
3131
|
+
var cbytes = await child.exportAsync(buildExportSettings());
|
|
3132
|
+
var cbase64 = figma.base64Encode(cbytes);
|
|
3133
|
+
regions.push({
|
|
3134
|
+
name: child.name,
|
|
3135
|
+
nodeId: child.id,
|
|
3136
|
+
type: child.type,
|
|
3137
|
+
image: {
|
|
3138
|
+
base64: cbase64,
|
|
3139
|
+
format: format,
|
|
3140
|
+
scale: scale,
|
|
3141
|
+
byteLength: cbytes.length,
|
|
3142
|
+
width: child.width,
|
|
3143
|
+
height: child.height,
|
|
3144
|
+
},
|
|
3145
|
+
});
|
|
3146
|
+
} catch (childErr) {
|
|
3147
|
+
regions.push({
|
|
3148
|
+
name: child.name,
|
|
3149
|
+
nodeId: child.id,
|
|
3150
|
+
error: childErr && childErr.message ? childErr.message : String(childErr),
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
figma.ui.postMessage({
|
|
3155
|
+
type: 'CAPTURE_SCREENSHOT_RESULT',
|
|
3156
|
+
requestId: msg.requestId,
|
|
3157
|
+
success: true,
|
|
3158
|
+
mode: 'regions',
|
|
3159
|
+
regionStrategy: 'children',
|
|
3160
|
+
regions: regions,
|
|
3161
|
+
totalRegions: regions.length,
|
|
3162
|
+
rootSummary: {
|
|
3163
|
+
nodeId: node.id,
|
|
3164
|
+
name: node.name,
|
|
3165
|
+
dimensions: { width: node.width, height: node.height },
|
|
3166
|
+
childrenCount: node.children.length,
|
|
3167
|
+
capped: node.children.length > maxRegions,
|
|
3168
|
+
},
|
|
3169
|
+
});
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
2851
3172
|
|
|
2852
|
-
//
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
3173
|
+
// ---------- v1.9.5 MODE: regions (slices strategy) — NOT SUPPORTED YET ----------
|
|
3174
|
+
if (returnMode === 'regions' && regionStrategy === 'slices') {
|
|
3175
|
+
// Figma exportAsync does not support crop regions natively.
|
|
3176
|
+
// Fallback: return a warning + single full image export.
|
|
3177
|
+
figma.ui.postMessage({
|
|
3178
|
+
type: 'CAPTURE_SCREENSHOT_RESULT',
|
|
3179
|
+
requestId: msg.requestId,
|
|
3180
|
+
success: false,
|
|
3181
|
+
error: "regionStrategy='slices' not supported in v1.9.5. Use regionStrategy='children' or returnMode='summary'.",
|
|
3182
|
+
});
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
// ---------- DEFAULT: full single export (base64 or file — server post-processes) ----------
|
|
3187
|
+
if (!('exportAsync' in node)) {
|
|
3188
|
+
throw new Error('Node type ' + node.type + ' does not support export');
|
|
2856
3189
|
}
|
|
2857
3190
|
|
|
2858
|
-
|
|
3191
|
+
var bytes = await node.exportAsync(buildExportSettings());
|
|
3192
|
+
var base64 = figma.base64Encode(bytes);
|
|
3193
|
+
var bounds = ('absoluteBoundingBox' in node) ? node.absoluteBoundingBox : null;
|
|
3194
|
+
|
|
3195
|
+
console.log('🌉 [F-MCP ATezer Bridge v1.9.5] Screenshot captured:', bytes.length, 'bytes (' + format + ' @' + scale + 'x)');
|
|
2859
3196
|
|
|
2860
3197
|
figma.ui.postMessage({
|
|
2861
3198
|
type: 'CAPTURE_SCREENSHOT_RESULT',
|
|
2862
3199
|
requestId: msg.requestId,
|
|
2863
3200
|
success: true,
|
|
3201
|
+
mode: returnMode, // 'file' or 'base64' (server post-processes 'file' mode)
|
|
2864
3202
|
image: {
|
|
2865
3203
|
base64: base64,
|
|
2866
3204
|
format: format,
|
|
2867
3205
|
scale: scale,
|
|
2868
3206
|
jpegQuality: format === 'JPG' ? jpegQuality : null,
|
|
2869
3207
|
byteLength: bytes.length,
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
},
|
|
2875
|
-
bounds: bounds
|
|
3208
|
+
width: node.width,
|
|
3209
|
+
height: node.height,
|
|
3210
|
+
node: { id: node.id, name: node.name, type: node.type },
|
|
3211
|
+
bounds: bounds,
|
|
2876
3212
|
}
|
|
2877
3213
|
});
|
|
2878
3214
|
|
package/f-mcp-plugin/ui.html
CHANGED
|
@@ -424,7 +424,7 @@
|
|
|
424
424
|
window.__figmaFileName = null;
|
|
425
425
|
// v1.8.0+: Plugin version reported in WebSocket "ready" handshake.
|
|
426
426
|
// Keep in sync with package.json, src/core/version.ts, f-mcp-plugin/code.js.
|
|
427
|
-
var FMCP_PLUGIN_VERSION = '1.9.
|
|
427
|
+
var FMCP_PLUGIN_VERSION = '1.9.5';
|
|
428
428
|
|
|
429
429
|
// v1.9.2+ Startup log — Figma plugin iframe'de yeni kod yüklendiğini doğrulamak için.
|
|
430
430
|
// Console'da bu log görünüyorsa plugin güncel. Görünmüyorsa iframe eski kopyayı tutuyor
|
package/package.json
CHANGED
|
@@ -119,6 +119,54 @@ active-ds.md `❌` ise: "Hangi DS? (SUI / Material / HIG / Kendi / Hiçbiri)". Y
|
|
|
119
119
|
|
|
120
120
|
Hardcoded `node.fills = [{ type: "SOLID", color: {...} }]` YASAK.
|
|
121
121
|
|
|
122
|
+
10a. **Inline Bind Verification (v1.9.4, ZORUNLU).** Her execute sonunda oluşturulan node'ları tara. Bind eksikse `throw` atılır — execute başarısız sayılır, Claude retry eder. Şablon:
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
// Execute'un sonunda, createdNodes listesini tarayıp bind kontrolü yap:
|
|
126
|
+
function assertBound(node) {
|
|
127
|
+
// Fill bind
|
|
128
|
+
if (Array.isArray(node.fills)) {
|
|
129
|
+
for (var i = 0; i < node.fills.length; i++) {
|
|
130
|
+
var f = node.fills[i];
|
|
131
|
+
if (f && f.visible !== false && f.type === "SOLID") {
|
|
132
|
+
var fbv = node.boundVariables && node.boundVariables.fills;
|
|
133
|
+
var bound = fbv && (Array.isArray(fbv) ? fbv[i] : true);
|
|
134
|
+
if (!bound) throw new Error("UNBOUND_FILL: " + node.name + " — setBoundVariableForPaint cagrisi eksik");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Padding/itemSpacing/radius bind (auto-layout frame)
|
|
139
|
+
if (node.type === "FRAME" || node.type === "COMPONENT") {
|
|
140
|
+
var padProps = ["paddingTop","paddingBottom","paddingLeft","paddingRight"];
|
|
141
|
+
for (var j = 0; j < padProps.length; j++) {
|
|
142
|
+
var p = padProps[j];
|
|
143
|
+
if (typeof node[p] === "number" && node[p] > 0 && !(node.boundVariables && node.boundVariables[p])) {
|
|
144
|
+
throw new Error("UNBOUND_PADDING: " + node.name + "." + p + "=" + node[p] + " — setBoundVariable cagrisi eksik");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (typeof node.itemSpacing === "number" && node.itemSpacing > 0 && !(node.boundVariables && node.boundVariables.itemSpacing)) {
|
|
148
|
+
throw new Error("UNBOUND_ITEMSPACING: " + node.name + " — setBoundVariable('itemSpacing', v) cagrisi eksik");
|
|
149
|
+
}
|
|
150
|
+
var radProps = ["cornerRadius","topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius"];
|
|
151
|
+
for (var k = 0; k < radProps.length; k++) {
|
|
152
|
+
var r = radProps[k];
|
|
153
|
+
if (typeof node[r] === "number" && node[r] > 0 && !(node.boundVariables && node.boundVariables[r])) {
|
|
154
|
+
throw new Error("UNBOUND_RADIUS: " + node.name + "." + r + "=" + node[r]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Text style
|
|
159
|
+
if (node.type === "TEXT" && !(node.textStyleId && typeof node.textStyleId === "string" && node.textStyleId !== "")) {
|
|
160
|
+
throw new Error("UNBOUND_TEXTSTYLE: '" + (node.characters||"").slice(0,30) + "' — setTextStyleIdAsync(style.id) cagrisi eksik");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Tüm oluşturulan node'ları doğrula:
|
|
165
|
+
for (var n = 0; n < createdNodes.length; n++) assertBound(createdNodes[n]);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Bu kontrol atlanırsa `figma_scan_ds_compliance` final gate'te sonuç zaten BLOCKING olur — ama inline check erkendedir ve context'i az yer. **v1.9.4 önerisi:** Her mega-step sonunda bu assertion bloğunu execute'un sonuna koy.
|
|
169
|
+
|
|
122
170
|
11. **appendChild sıralaması kritik.** ÖNCE `parent.appendChild(child)`, SONRA `child.layoutSizingHorizontal = "FILL"` / `layoutPositioning = "ABSOLUTE"`:
|
|
123
171
|
```js
|
|
124
172
|
parent.appendChild(child); // ÖNCE
|
|
@@ -136,6 +184,26 @@ active-ds.md `❌` ise: "Hangi DS? (SUI / Material / HIG / Kendi / Hiçbiri)". Y
|
|
|
136
184
|
|
|
137
185
|
16. **Her Promise'i `await` et.**
|
|
138
186
|
|
|
187
|
+
17. **v1.9.5 Concise Query Rule — tek satır boyut/metadata sorguları.**
|
|
188
|
+
|
|
189
|
+
Boyut, renk, layout sorgulamak için **20+ satır JS YAZMA**. Tek satır yeter:
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
// İYİ — tek satır, minimal context
|
|
193
|
+
return await figma.getNodeByIdAsync(id).then(n => ({ w: n.width, h: n.height, children: n.children?.length }));
|
|
194
|
+
|
|
195
|
+
// İYİ — toplu (birden fazla node)
|
|
196
|
+
return Promise.all(ids.map(async id => {
|
|
197
|
+
const n = await figma.getNodeByIdAsync(id);
|
|
198
|
+
return { id, name: n?.name, size: n ? [n.width, n.height] : null };
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
// KÖTÜ — 30 satır "defensive" kod boyut sorgusu için
|
|
202
|
+
// await figma.loadAllPagesAsync(); for (var pg of ...) { if (...) ... }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Kural:** Tek nodeId için size → 1 satır. Multi-node metadata → 1 Promise.all. Complex traversal sadece üretim mantığında gerekli — keşifte değil.
|
|
206
|
+
|
|
139
207
|
## 2. Sayfa Kuralları
|
|
140
208
|
|
|
141
209
|
```js
|
|
@@ -226,6 +226,15 @@ FOR each input in required_inputs:
|
|
|
226
226
|
|
|
227
227
|
Missing list'teki tüm soruları **TEK** `AskUserQuestion` çağrısında toplayıp sor. Max 4 soru/çağrı (AskUserQuestion limiti). 5+ soru gerekiyorsa 2 ayrı çağrı yap, ama ÖNCE kritik 4'ü sor, sonra kalanları.
|
|
228
228
|
|
|
229
|
+
### v1.9.5 Elicitation Kuralı (SERT)
|
|
230
|
+
|
|
231
|
+
- **Maks 1 `AskUserQuestion` çağrısı** tüm oturum boyunca. Tekrar soru sormak yasak — state'ten veya context'ten çıkar.
|
|
232
|
+
- **"devam et" / "tamam" / "ok" / "yap" sonrası soru YASAK.** Kullanıcı onay verdi → üretime geç.
|
|
233
|
+
- User prompt'u anlamlıysa (spesifik ekran adı, boyut, DS veriliyorsa) **hiç sorma**, direkt planla ve göster.
|
|
234
|
+
- **"Sen seç"** cevabı alırsan: mantıklı default kullan (iPhone 17, mobile, single variant, active DS).
|
|
235
|
+
- Sorulan her soru ≤30 kelime — uzun elicitation yasak.
|
|
236
|
+
- Soruyu sormanın context maliyeti ≥ yanıtın değeri mi? Değilse sorma.
|
|
237
|
+
|
|
229
238
|
**Örnek — generate-figma-screen:**
|
|
230
239
|
|
|
231
240
|
```
|
|
@@ -144,6 +144,35 @@ Benchmark/görselden DEĞER alma YASAK. Sadece NİYET: layout yönü, hiyerarşi
|
|
|
144
144
|
2. **Content** — DS instance yerleşimi → screenshot → onay
|
|
145
145
|
3. **Polish** — spacing, states, edge cases → son screenshot → audit
|
|
146
146
|
|
|
147
|
+
### v1.9.5 Discovery Budget Rule (SERT)
|
|
148
|
+
|
|
149
|
+
- **Maks 3 discovery çağrısı** (figma_get_*, figma_search_*, figma_execute read-only) sonra plan sun.
|
|
150
|
+
- Plan kullanıcıya 1-2 cümle + varsa özet: "Şu ekran/section'ları oluşturacağım: [liste]. Onay veriyor musun?"
|
|
151
|
+
- Kullanıcı onay verdikten sonra **mutation** aşamasına geç (figma_execute createFrame/setFills/setBoundVariable) — discovery counter reset olur.
|
|
152
|
+
- Plugin 8 çağrıdan sonra `_warnings: ["DISCOVERY_BUDGET_WARNING..."]` döner — görünce üretime geçmek zorundasın.
|
|
153
|
+
- 12 çağrıdan sonra `_DISCOVERY_BUDGET_EXCEEDED_BLOCKING: true` döner — **skip edilemez**, plan sun veya dur.
|
|
154
|
+
|
|
155
|
+
### v1.9.5 Screenshot Method Selection (KARAR AĞACI)
|
|
156
|
+
|
|
157
|
+
Ekran yakalama isteği olduğunda şu ağacı takip et:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
İhtiyaç ne?
|
|
161
|
+
├── "Planlama yapacağım, layout anlamak istiyorum" → returnMode: "summary" (metadata, screenshotsuz)
|
|
162
|
+
├── "Kullanıcıya son halini göstereyim" → returnMode: "file" (1 screenshot dosyaya)
|
|
163
|
+
├── "Büyük/scroll'lu ekran, bölümleri görmek istiyorum" → returnMode: "regions", regionStrategy: "children"
|
|
164
|
+
├── "Üretim sonrası hızlı validation" → returnMode: "file"
|
|
165
|
+
├── "Spesifik region (örn Hero)" → single-node file veya regions children maxRegions=3
|
|
166
|
+
└── "Base64 context'te gerekli (nadiren)" → returnMode: "base64" (explicit, _warning ile)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Budget farkındalığı:**
|
|
170
|
+
- Bir oturumda >3 farklı nodeId için screenshot → 4. için zorunlu `summary` veya `regions`
|
|
171
|
+
- Response'da `_warnings: ["CONTEXT_NEAR_LIMIT"]` veya `"DISCOVERY_BUDGET_..."` görürsen → sonraki screenshot zorunlu `summary`
|
|
172
|
+
- Pixel-perfect değil, **region-perfect** düşün: "Hero'yu göreyim, gerekirse Actions'ı da" — hepsi birden değil
|
|
173
|
+
|
|
174
|
+
**Kritik:** Screenshot YASAK değil. "Yasak" yerine **doğru yöntem** kullan. Her mode'un kullanım amacı var.
|
|
175
|
+
|
|
147
176
|
### Error Recovery
|
|
148
177
|
|
|
149
178
|
| Hata | Aksiyon |
|