@atezer/figma-mcp-bridge 1.9.4 → 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 +56 -0
- package/README.md +19 -2
- 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 +5 -0
- package/dist/core/plugin-bridge-connector.d.ts.map +1 -1
- 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 +170 -21
- package/dist/local-plugin-only.js.map +1 -1
- package/f-mcp-plugin/code.js +198 -32
- package/f-mcp-plugin/ui.html +1 -1
- package/package.json +1 -1
- package/skills/figma-canvas-ops/SKILL.md +20 -0
- package/skills/fmcp-intent-router/SKILL.md +9 -0
- package/skills/fmcp-screen-orchestrator/SKILL.md +29 -0
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 = [];
|
|
@@ -2985,64 +3075,140 @@ figma.ui.onmessage = async (msg) => {
|
|
|
2985
3075
|
// ============================================================================
|
|
2986
3076
|
else if (msg.type === 'CAPTURE_SCREENSHOT') {
|
|
2987
3077
|
try {
|
|
2988
|
-
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);
|
|
2989
3079
|
|
|
2990
3080
|
var node = msg.nodeId ? await figma.getNodeByIdAsync(msg.nodeId) : figma.currentPage;
|
|
2991
3081
|
if (!node) {
|
|
2992
3082
|
throw new Error('Node not found: ' + msg.nodeId);
|
|
2993
3083
|
}
|
|
2994
3084
|
|
|
2995
|
-
//
|
|
2996
|
-
if (!('exportAsync' in node)) {
|
|
2997
|
-
throw new Error('Node type ' + node.type + ' does not support export');
|
|
2998
|
-
}
|
|
2999
|
-
|
|
3000
|
-
// v1.8.0: Default JPG@1x q70 for context safety. Plugin reads msg.format,
|
|
3001
|
-
// msg.scale, msg.jpegQuality from the request payload (passed through
|
|
3002
|
-
// captureScreenshot connector → bridge → here).
|
|
3085
|
+
// v1.8.0: Default JPG@1x q70 for context safety.
|
|
3003
3086
|
var format = (msg.format || 'JPG').toUpperCase();
|
|
3004
3087
|
var scale = msg.scale != null ? msg.scale : 1;
|
|
3005
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;
|
|
3006
3093
|
|
|
3007
|
-
|
|
3008
|
-
format: format,
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
// JPG quality (Figma Plugin API supports 'quality' for ExportSettingsImage)
|
|
3012
|
-
if (format === 'JPG') {
|
|
3013
|
-
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;
|
|
3014
3098
|
}
|
|
3015
3099
|
|
|
3016
|
-
//
|
|
3017
|
-
|
|
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
|
+
}
|
|
3018
3113
|
|
|
3019
|
-
//
|
|
3020
|
-
|
|
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
|
+
}
|
|
3021
3172
|
|
|
3022
|
-
//
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
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;
|
|
3026
3184
|
}
|
|
3027
3185
|
|
|
3028
|
-
|
|
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');
|
|
3189
|
+
}
|
|
3190
|
+
|
|
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)');
|
|
3029
3196
|
|
|
3030
3197
|
figma.ui.postMessage({
|
|
3031
3198
|
type: 'CAPTURE_SCREENSHOT_RESULT',
|
|
3032
3199
|
requestId: msg.requestId,
|
|
3033
3200
|
success: true,
|
|
3201
|
+
mode: returnMode, // 'file' or 'base64' (server post-processes 'file' mode)
|
|
3034
3202
|
image: {
|
|
3035
3203
|
base64: base64,
|
|
3036
3204
|
format: format,
|
|
3037
3205
|
scale: scale,
|
|
3038
3206
|
jpegQuality: format === 'JPG' ? jpegQuality : null,
|
|
3039
3207
|
byteLength: bytes.length,
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
},
|
|
3045
|
-
bounds: bounds
|
|
3208
|
+
width: node.width,
|
|
3209
|
+
height: node.height,
|
|
3210
|
+
node: { id: node.id, name: node.name, type: node.type },
|
|
3211
|
+
bounds: bounds,
|
|
3046
3212
|
}
|
|
3047
3213
|
});
|
|
3048
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
|
@@ -184,6 +184,26 @@ active-ds.md `❌` ise: "Hangi DS? (SUI / Material / HIG / Kendi / Hiçbiri)". Y
|
|
|
184
184
|
|
|
185
185
|
16. **Her Promise'i `await` et.**
|
|
186
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
|
+
|
|
187
207
|
## 2. Sayfa Kuralları
|
|
188
208
|
|
|
189
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 |
|