figma-rust-mcp 3.0.3 → 3.0.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/package.json +1 -1
- package/plugin/code.js +595 -84
- package/plugin/ui.html +122 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-rust-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.5",
|
|
4
4
|
"description": "Claude Code to Figma — Bidirectional MCP bridge. AI draws UI on Figma canvas, reads designs back. Works with Claude, Cursor, VS Code, Windsurf, Antigravity. No API key needed.",
|
|
5
5
|
"mcpName": "io.github.BuiHung1612/figma-mcp",
|
|
6
6
|
"type": "module",
|
package/plugin/code.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// figma-mcp — Figma Plugin main thread v1.2.1
|
|
2
2
|
// Handles both WRITE (draw UI) and READ (extract design) operations.
|
|
3
3
|
// AUTO-GENERATED by scripts/build-plugin.js — edit plugin-src/*.js instead
|
|
4
|
-
// Generated: 2026-08-
|
|
4
|
+
// Generated: 2026-08-30T10:25:26.211Z
|
|
5
5
|
|
|
6
6
|
// ── plugin-src/utils.js ──
|
|
7
7
|
// ─── UTILS ────────────────────────────────────────────────────────────────────
|
|
@@ -347,53 +347,129 @@ function cacheNode(node) {
|
|
|
347
347
|
return node;
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
function normalizeNodeId(id) {
|
|
351
|
+
if (!id || typeof id !== "string") return id;
|
|
352
|
+
var clean = id.trim().replace(/^['"]|['"]$/g, "");
|
|
353
|
+
try { clean = decodeURIComponent(clean); } catch(e) {}
|
|
354
|
+
// Convert URL hyphenated IDs (e.g. 2715-40862) to colon notation (2715:40862)
|
|
355
|
+
clean = clean.replace(/(\d+)-(\d+)/g, "$1:$2");
|
|
356
|
+
return clean;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
var allPagesLoaded = false;
|
|
360
|
+
async function ensureAllPagesLoaded() {
|
|
361
|
+
if (allPagesLoaded) return;
|
|
362
|
+
if (typeof figma.loadAllPagesAsync === "function") {
|
|
363
|
+
try {
|
|
364
|
+
await figma.loadAllPagesAsync();
|
|
365
|
+
allPagesLoaded = true;
|
|
366
|
+
} catch(e) {}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function getNodeNotFoundContext(id, name) {
|
|
371
|
+
var currentPageName = figma.currentPage ? figma.currentPage.name : "unknown";
|
|
372
|
+
var availablePages = figma.root && figma.root.children ? figma.root.children.map(function(p) { return p.name; }).join(", ") : "none";
|
|
373
|
+
var selInfo = figma.currentPage && figma.currentPage.selection && figma.currentPage.selection.length > 0
|
|
374
|
+
? figma.currentPage.selection.map(function(s) { return s.id + " (" + s.name + ")"; }).join(", ")
|
|
375
|
+
: "none";
|
|
376
|
+
var target = id || name || "no id/name given";
|
|
377
|
+
return "Node not found: " + target + ". [Active Page: \"" + currentPageName + "\", Available Pages: [" + availablePages + "], Current Selection: [" + selInfo + "]]. Use figma_read get_page_nodes to list nodes or set_page to switch pages.";
|
|
378
|
+
}
|
|
379
|
+
|
|
350
380
|
function findNodeById(id) {
|
|
351
381
|
if (!id) return null;
|
|
352
|
-
|
|
353
|
-
if (figma.
|
|
354
|
-
if (
|
|
355
|
-
|
|
382
|
+
var cleanId = normalizeNodeId(id);
|
|
383
|
+
if (figma.currentPage.id === cleanId || figma.currentPage.id === id) return figma.currentPage;
|
|
384
|
+
if (figma.root.id === cleanId || figma.root.id === id) return figma.root;
|
|
385
|
+
if (nodeCache.has(cleanId)) {
|
|
386
|
+
var cached = nodeCache.get(cleanId);
|
|
356
387
|
if (!cached.removed) return cached;
|
|
388
|
+
nodeCache.delete(cleanId);
|
|
389
|
+
}
|
|
390
|
+
if (nodeCache.has(id)) {
|
|
391
|
+
var cached2 = nodeCache.get(id);
|
|
392
|
+
if (!cached2.removed) return cached2;
|
|
357
393
|
nodeCache.delete(id);
|
|
358
394
|
}
|
|
359
395
|
try {
|
|
360
396
|
if (typeof figma.getNodeById === "function") {
|
|
361
|
-
var node = figma.getNodeById(id);
|
|
397
|
+
var node = figma.getNodeById(cleanId) || (cleanId !== id ? figma.getNodeById(id) : null);
|
|
362
398
|
if (node && !node.removed) return cacheNode(node);
|
|
363
399
|
}
|
|
364
400
|
} catch(e) {}
|
|
365
401
|
// Check selection
|
|
366
402
|
for (var i = 0; i < figma.currentPage.selection.length; i++) {
|
|
367
|
-
|
|
403
|
+
var sel = figma.currentPage.selection[i];
|
|
404
|
+
if (sel.id === cleanId || sel.id === id) return cacheNode(sel);
|
|
368
405
|
}
|
|
369
406
|
return null;
|
|
370
407
|
}
|
|
371
408
|
|
|
372
409
|
async function findNodeByIdAsync(id) {
|
|
373
410
|
if (!id) return null;
|
|
374
|
-
|
|
375
|
-
if (figma.
|
|
376
|
-
if (
|
|
377
|
-
|
|
411
|
+
var cleanId = normalizeNodeId(id);
|
|
412
|
+
if (figma.currentPage.id === cleanId || figma.currentPage.id === id) return figma.currentPage;
|
|
413
|
+
if (figma.root.id === cleanId || figma.root.id === id) return figma.root;
|
|
414
|
+
if (nodeCache.has(cleanId)) {
|
|
415
|
+
var cached = nodeCache.get(cleanId);
|
|
378
416
|
if (!cached.removed) return cached;
|
|
417
|
+
nodeCache.delete(cleanId);
|
|
418
|
+
}
|
|
419
|
+
if (nodeCache.has(id)) {
|
|
420
|
+
var cached2 = nodeCache.get(id);
|
|
421
|
+
if (!cached2.removed) return cached2;
|
|
379
422
|
nodeCache.delete(id);
|
|
380
423
|
}
|
|
381
|
-
|
|
424
|
+
|
|
425
|
+
// 1. Try synchronous lookup first (0ms)
|
|
382
426
|
try {
|
|
383
427
|
if (typeof figma.getNodeById === "function") {
|
|
384
|
-
var syncNode = figma.getNodeById(id);
|
|
428
|
+
var syncNode = figma.getNodeById(cleanId) || (cleanId !== id ? figma.getNodeById(id) : null);
|
|
385
429
|
if (syncNode && !syncNode.removed) return cacheNode(syncNode);
|
|
386
430
|
}
|
|
387
431
|
} catch(e) {}
|
|
388
|
-
|
|
432
|
+
|
|
433
|
+
// 2. Try async lookup
|
|
389
434
|
try {
|
|
390
|
-
var node = await figma.getNodeByIdAsync(
|
|
435
|
+
var node = await figma.getNodeByIdAsync(cleanId);
|
|
436
|
+
if (!node && cleanId !== id) node = await figma.getNodeByIdAsync(id);
|
|
391
437
|
if (node && !node.removed) return cacheNode(node);
|
|
392
438
|
} catch(e) {}
|
|
439
|
+
|
|
440
|
+
// 3. Check selection
|
|
441
|
+
for (var i = 0; i < figma.currentPage.selection.length; i++) {
|
|
442
|
+
var s = figma.currentPage.selection[i];
|
|
443
|
+
if (s.id === cleanId || s.id === id) return cacheNode(s);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 4. Fallback: If not found and pages are lazily loaded, load all pages and retry
|
|
447
|
+
if (!allPagesLoaded && typeof figma.loadAllPagesAsync === "function") {
|
|
448
|
+
await ensureAllPagesLoaded();
|
|
449
|
+
try {
|
|
450
|
+
var retryNode = await figma.getNodeByIdAsync(cleanId);
|
|
451
|
+
if (!retryNode && cleanId !== id) retryNode = await figma.getNodeByIdAsync(id);
|
|
452
|
+
if (retryNode && !retryNode.removed) return cacheNode(retryNode);
|
|
453
|
+
} catch(e) {}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 5. Fallback for instance sub-layer IDs (e.g. "I2715:40862;123:456")
|
|
457
|
+
if (cleanId.indexOf(";") !== -1) {
|
|
458
|
+
var parts = cleanId.split(";");
|
|
459
|
+
for (var p = 0; p < parts.length; p++) {
|
|
460
|
+
var partId = parts[p].replace(/^I/, "");
|
|
461
|
+
try {
|
|
462
|
+
var subNode = await figma.getNodeByIdAsync(partId);
|
|
463
|
+
if (subNode && !subNode.removed) return cacheNode(subNode);
|
|
464
|
+
} catch(e) {}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
393
468
|
return null;
|
|
394
469
|
}
|
|
395
470
|
|
|
396
471
|
function findNodeByName(name) {
|
|
472
|
+
if (!name) return null;
|
|
397
473
|
if (figma.currentPage.name === name) return figma.currentPage;
|
|
398
474
|
// Search in selection first, then shallow children, then findOne as fallback
|
|
399
475
|
for (var i = 0; i < figma.currentPage.selection.length; i++) {
|
|
@@ -402,20 +478,34 @@ function findNodeByName(name) {
|
|
|
402
478
|
for (var j = 0; j < figma.currentPage.children.length; j++) {
|
|
403
479
|
if (figma.currentPage.children[j].name === name) return cacheNode(figma.currentPage.children[j]);
|
|
404
480
|
}
|
|
405
|
-
var found = figma.currentPage.findOne(n
|
|
406
|
-
if (found) cacheNode(found);
|
|
407
|
-
|
|
481
|
+
var found = figma.currentPage.findOne(function(n) { return n.name === name; });
|
|
482
|
+
if (found) return cacheNode(found);
|
|
483
|
+
|
|
484
|
+
// Cross-page fallback
|
|
485
|
+
if (figma.root && figma.root.children) {
|
|
486
|
+
for (var p = 0; p < figma.root.children.length; p++) {
|
|
487
|
+
var page = figma.root.children[p];
|
|
488
|
+
if (page === figma.currentPage) continue;
|
|
489
|
+
if (page.name === name) return page;
|
|
490
|
+
var pageFound = page.findOne ? page.findOne(function(n) { return n.name === name; }) : null;
|
|
491
|
+
if (pageFound) return cacheNode(pageFound);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return null;
|
|
408
496
|
}
|
|
409
497
|
|
|
410
498
|
async function resolveNode(params) {
|
|
411
|
-
|
|
412
|
-
var
|
|
499
|
+
if (!params) return null;
|
|
500
|
+
var id = params.id || params.nodeId || params.node_id || params.targetId || params.target_id;
|
|
501
|
+
var name = params.name || params.nodeName || params.node_name;
|
|
413
502
|
var node = null;
|
|
414
503
|
if (id) node = await findNodeByIdAsync(id);
|
|
415
504
|
if (!node && name) node = findNodeByName(name);
|
|
416
505
|
return node;
|
|
417
506
|
}
|
|
418
507
|
|
|
508
|
+
|
|
419
509
|
function nodeToInfo(node) {
|
|
420
510
|
if (!node) return null;
|
|
421
511
|
const info = {
|
|
@@ -438,6 +528,71 @@ function yieldToUI(delayMs) {
|
|
|
438
528
|
});
|
|
439
529
|
}
|
|
440
530
|
|
|
531
|
+
// Flexible operation handler resolver with camelCase/snake_case and alias support
|
|
532
|
+
function resolveOperationHandler(operation) {
|
|
533
|
+
if (!operation || typeof operation !== "string") return null;
|
|
534
|
+
if (typeof handlers !== "object" || !handlers) return null;
|
|
535
|
+
if (handlers[operation]) return handlers[operation];
|
|
536
|
+
|
|
537
|
+
// 1. Convert snake_case -> camelCase (e.g. create_variable -> createVariable, list_pages -> listPages)
|
|
538
|
+
var camel = operation.replace(/_([a-z0-9])/gi, function(_, c) { return c.toUpperCase(); });
|
|
539
|
+
if (handlers[camel]) return handlers[camel];
|
|
540
|
+
|
|
541
|
+
// 2. Convert camelCase -> snake_case (e.g. getPageNodes -> get_page_nodes, getNodeDetail -> get_node_detail)
|
|
542
|
+
var snake = operation.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
543
|
+
if (handlers[snake]) return handlers[snake];
|
|
544
|
+
|
|
545
|
+
// 3. Common semantic synonyms & aliases
|
|
546
|
+
var normalized = operation.toLowerCase().replace(/[-_\s]+/g, "");
|
|
547
|
+
var aliasMap = {
|
|
548
|
+
"getnode": handlers.get_node_detail,
|
|
549
|
+
"getnodedetail": handlers.get_node_detail,
|
|
550
|
+
"getnodeinfo": handlers.get_node_detail,
|
|
551
|
+
"nodedetail": handlers.get_node_detail,
|
|
552
|
+
"nodeinfo": handlers.get_node_detail,
|
|
553
|
+
"inspectnode": handlers.get_design_context,
|
|
554
|
+
"inspect": handlers.get_design_context,
|
|
555
|
+
"getdesigncontext": handlers.get_design_context,
|
|
556
|
+
"designcontext": handlers.get_design_context,
|
|
557
|
+
"getdesign": handlers.get_design,
|
|
558
|
+
"getselection": handlers.get_selection,
|
|
559
|
+
"selection": handlers.get_selection,
|
|
560
|
+
"getpagenodes": handlers.get_page_nodes,
|
|
561
|
+
"pagenodes": handlers.get_page_nodes,
|
|
562
|
+
"getstyles": handlers.get_styles,
|
|
563
|
+
"styles": handlers.get_styles,
|
|
564
|
+
"getvariables": handlers.get_variables,
|
|
565
|
+
"gettokens": handlers.get_variables,
|
|
566
|
+
"tokens": handlers.get_variables,
|
|
567
|
+
"variables": handlers.get_variables,
|
|
568
|
+
"getvariabletokens": handlers.get_variables,
|
|
569
|
+
"getlocalcomponents": handlers.get_local_components,
|
|
570
|
+
"localcomponents": handlers.get_local_components,
|
|
571
|
+
"listcomponents": handlers.listComponents,
|
|
572
|
+
"components": handlers.get_local_components,
|
|
573
|
+
"getcomponentmap": handlers.get_component_map,
|
|
574
|
+
"getunmappedcomponents": handlers.get_unmapped_components,
|
|
575
|
+
"scandesign": handlers.scan_design,
|
|
576
|
+
"searchnodes": handlers.search_nodes,
|
|
577
|
+
"getviewport": handlers.get_viewport,
|
|
578
|
+
"setviewport": handlers.set_viewport,
|
|
579
|
+
"exportsvg": handlers.export_svg,
|
|
580
|
+
"exportimage": handlers.export_image,
|
|
581
|
+
"exportassets": handlers.export_assets,
|
|
582
|
+
"listpages": handlers.listPages,
|
|
583
|
+
"setpage": handlers.setPage,
|
|
584
|
+
"createpage": handlers.createPage,
|
|
585
|
+
"loadallpages": handlers.loadAllPagesAsync,
|
|
586
|
+
"loadallpagesasync": handlers.loadAllPagesAsync,
|
|
587
|
+
"update": handlers.modify,
|
|
588
|
+
"remove": handlers["delete"]
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
if (aliasMap[normalized]) return aliasMap[normalized];
|
|
592
|
+
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
|
|
441
596
|
|
|
442
597
|
// ── plugin-src/svg-path-helpers.js ──
|
|
443
598
|
// ─── SVG PATH NORMALIZATION ───────────────────────────────────────────────────
|
|
@@ -868,6 +1023,150 @@ function resolveTextStyle(node, opts) {
|
|
|
868
1023
|
return out;
|
|
869
1024
|
}
|
|
870
1025
|
|
|
1026
|
+
// ── Design Token / Variable Resolver Map ──
|
|
1027
|
+
var cachedVariableResolverMap = null;
|
|
1028
|
+
var cachedVariableResolverMapTime = 0;
|
|
1029
|
+
|
|
1030
|
+
async function buildVariableResolverMapAsync(forceRefresh) {
|
|
1031
|
+
var now = Date.now();
|
|
1032
|
+
if (!forceRefresh && cachedVariableResolverMap && (now - cachedVariableResolverMapTime < 15000)) {
|
|
1033
|
+
return cachedVariableResolverMap;
|
|
1034
|
+
}
|
|
1035
|
+
var map = { byId: {}, byName: {} };
|
|
1036
|
+
try {
|
|
1037
|
+
if (typeof figma.variables !== "undefined" && typeof figma.variables.getLocalVariablesAsync === "function") {
|
|
1038
|
+
var vars = await figma.variables.getLocalVariablesAsync();
|
|
1039
|
+
var rawMap = {};
|
|
1040
|
+
for (var i = 0; i < vars.length; i++) {
|
|
1041
|
+
var v = vars[i];
|
|
1042
|
+
if (v && v.id) rawMap[v.id] = v;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function resolveVarValue(v, depth) {
|
|
1046
|
+
if (!v || depth > 6) return null;
|
|
1047
|
+
var defaultVal = null;
|
|
1048
|
+
if (v.valuesByMode) {
|
|
1049
|
+
var modeKeys = Object.keys(v.valuesByMode);
|
|
1050
|
+
if (modeKeys.length > 0) defaultVal = v.valuesByMode[modeKeys[0]];
|
|
1051
|
+
}
|
|
1052
|
+
if (defaultVal && typeof defaultVal === "object" && defaultVal.type === "VARIABLE_ALIAS" && defaultVal.id) {
|
|
1053
|
+
var targetVar = rawMap[defaultVal.id];
|
|
1054
|
+
if (targetVar) {
|
|
1055
|
+
var targetRes = resolveVarValue(targetVar, depth + 1);
|
|
1056
|
+
return {
|
|
1057
|
+
isAlias: true,
|
|
1058
|
+
targetId: targetVar.id,
|
|
1059
|
+
targetName: targetVar.name,
|
|
1060
|
+
primitiveName: (targetRes && targetRes.primitiveName) ? targetRes.primitiveName : targetVar.name,
|
|
1061
|
+
resolvedValue: targetRes ? targetRes.resolvedValue : null,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (defaultVal && typeof defaultVal === "object" && "r" in defaultVal && "g" in defaultVal && "b" in defaultVal) {
|
|
1066
|
+
return { isAlias: false, resolvedValue: rgbToHex(defaultVal), primitiveName: v.name };
|
|
1067
|
+
}
|
|
1068
|
+
if (typeof defaultVal === "number" || typeof defaultVal === "string" || typeof defaultVal === "boolean") {
|
|
1069
|
+
return { isAlias: false, resolvedValue: defaultVal, primitiveName: v.name };
|
|
1070
|
+
}
|
|
1071
|
+
return { isAlias: false, resolvedValue: defaultVal, primitiveName: v.name };
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
for (var j = 0; j < vars.length; j++) {
|
|
1075
|
+
var vr = vars[j];
|
|
1076
|
+
if (!vr || !vr.id) continue;
|
|
1077
|
+
var res = resolveVarValue(vr, 0);
|
|
1078
|
+
var item = {
|
|
1079
|
+
id: vr.id,
|
|
1080
|
+
name: vr.name,
|
|
1081
|
+
resolvedType: vr.resolvedType,
|
|
1082
|
+
isAlias: res ? res.isAlias : false,
|
|
1083
|
+
targetName: res ? res.targetName : null,
|
|
1084
|
+
primitiveName: res ? res.primitiveName : vr.name,
|
|
1085
|
+
resolvedValue: res ? res.resolvedValue : null,
|
|
1086
|
+
};
|
|
1087
|
+
map.byId[vr.id] = item;
|
|
1088
|
+
map.byName[vr.name] = item;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
} catch(e) {}
|
|
1092
|
+
cachedVariableResolverMap = map;
|
|
1093
|
+
cachedVariableResolverMapTime = now;
|
|
1094
|
+
return map;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// ── Clean Variant & Component Properties Parser ──
|
|
1098
|
+
function cleanComponentProperties(node) {
|
|
1099
|
+
var out = { variant: null, props: null };
|
|
1100
|
+
if (!node) return out;
|
|
1101
|
+
|
|
1102
|
+
// 1. Native variantProperties on instance
|
|
1103
|
+
if (node.variantProperties && typeof node.variantProperties === "object") {
|
|
1104
|
+
var varKeys = Object.keys(node.variantProperties);
|
|
1105
|
+
if (varKeys.length > 0) {
|
|
1106
|
+
out.variant = {};
|
|
1107
|
+
for (var vk = 0; vk < varKeys.length; vk++) {
|
|
1108
|
+
out.variant[varKeys[vk]] = node.variantProperties[varKeys[vk]];
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// 2. Component Properties parsing (strip #ID suffix and separate variants vs normal props)
|
|
1114
|
+
if (node.componentProperties && typeof node.componentProperties === "object") {
|
|
1115
|
+
var rawProps = node.componentProperties;
|
|
1116
|
+
var propKeys = Object.keys(rawProps);
|
|
1117
|
+
if (propKeys.length > 0) {
|
|
1118
|
+
var cleanProps = {};
|
|
1119
|
+
var inferredVariants = out.variant || {};
|
|
1120
|
+
for (var pk = 0; pk < propKeys.length; pk++) {
|
|
1121
|
+
var rawKey = propKeys[pk];
|
|
1122
|
+
var propObj = rawProps[rawKey];
|
|
1123
|
+
if (!propObj) continue;
|
|
1124
|
+
var cleanKey = rawKey.split("#")[0].trim();
|
|
1125
|
+
if (propObj.type === "VARIANT") {
|
|
1126
|
+
inferredVariants[cleanKey] = propObj.value;
|
|
1127
|
+
} else {
|
|
1128
|
+
cleanProps[cleanKey] = propObj.value;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
if (Object.keys(cleanProps).length > 0) out.props = cleanProps;
|
|
1132
|
+
if (Object.keys(inferredVariants).length > 0) out.variant = inferredVariants;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return out;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// ── Infer Semantic Role ──
|
|
1139
|
+
function inferSemanticRole(node, info) {
|
|
1140
|
+
if (!node) return null;
|
|
1141
|
+
var name = (node.name || "").toLowerCase();
|
|
1142
|
+
var type = node.type;
|
|
1143
|
+
|
|
1144
|
+
if (isLikelyIcon(node) || name.indexOf("icon") !== -1) return "icon";
|
|
1145
|
+
if (name.indexOf("button") !== -1 || name.indexOf("btn") !== -1) return "button";
|
|
1146
|
+
if (name.indexOf("input") !== -1 || name.indexOf("textfield") !== -1 || name.indexOf("search") !== -1) return "input";
|
|
1147
|
+
if (name.indexOf("badge") !== -1 || name.indexOf("tag") !== -1 || name.indexOf("chip") !== -1 || name.indexOf("pill") !== -1) return "badge";
|
|
1148
|
+
if (name.indexOf("avatar") !== -1 || name.indexOf("userpic") !== -1 || name.indexOf("profile-pic") !== -1) return "avatar";
|
|
1149
|
+
if (name.indexOf("card") !== -1) return "card";
|
|
1150
|
+
if (name.indexOf("modal") !== -1 || name.indexOf("dialog") !== -1 || name.indexOf("popup") !== -1) return "modal";
|
|
1151
|
+
if (name.indexOf("checkbox") !== -1) return "checkbox";
|
|
1152
|
+
if (name.indexOf("switch") !== -1 || name.indexOf("toggle") !== -1) return "switch";
|
|
1153
|
+
if (name.indexOf("divider") !== -1 || name.indexOf("separator") !== -1) return "divider";
|
|
1154
|
+
if (name.indexOf("header") !== -1 || name.indexOf("navbar") !== -1 || name.indexOf("appbar") !== -1) return "header";
|
|
1155
|
+
if (name.indexOf("tab") !== -1) return "tab";
|
|
1156
|
+
|
|
1157
|
+
if (type === "INSTANCE") {
|
|
1158
|
+
if (info && info.componentSetName) {
|
|
1159
|
+
var setName = info.componentSetName.toLowerCase();
|
|
1160
|
+
if (setName.indexOf("button") !== -1) return "button";
|
|
1161
|
+
if (setName.indexOf("input") !== -1) return "input";
|
|
1162
|
+
if (setName.indexOf("badge") !== -1) return "badge";
|
|
1163
|
+
if (setName.indexOf("avatar") !== -1) return "avatar";
|
|
1164
|
+
if (setName.indexOf("icon") !== -1) return "icon";
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
871
1170
|
// Instance → main component resolution.
|
|
872
1171
|
//
|
|
873
1172
|
// `instance.mainComponent` throws under documentAccess: dynamic-page, so the
|
|
@@ -908,10 +1207,18 @@ async function resolveInstanceComponents(entries, apply) {
|
|
|
908
1207
|
}));
|
|
909
1208
|
for (var b = 0; b < batch.length; b++) {
|
|
910
1209
|
var main = mains[b];
|
|
911
|
-
|
|
912
|
-
var
|
|
913
|
-
if (
|
|
914
|
-
|
|
1210
|
+
var entryNode = batch[b].node;
|
|
1211
|
+
var entryInfo = batch[b].info;
|
|
1212
|
+
if (main) {
|
|
1213
|
+
var desc = describeMainComponent(main);
|
|
1214
|
+
if (apply) apply(entryInfo, desc);
|
|
1215
|
+
else Object.assign(entryInfo, desc);
|
|
1216
|
+
}
|
|
1217
|
+
var parsed = cleanComponentProperties(entryNode);
|
|
1218
|
+
if (parsed.variant) entryInfo.variant = parsed.variant;
|
|
1219
|
+
if (parsed.props) entryInfo.props = parsed.props;
|
|
1220
|
+
var role = inferSemanticRole(entryNode, entryInfo);
|
|
1221
|
+
if (role) entryInfo.role = role;
|
|
915
1222
|
resolved++;
|
|
916
1223
|
}
|
|
917
1224
|
}
|
|
@@ -1132,19 +1439,41 @@ function extractDesignTree(node, depth, maxDepth, detailLevel, filterInvisible,
|
|
|
1132
1439
|
try { if ("blendMode" in node && node.blendMode !== "NORMAL" && node.blendMode !== "PASS_THROUGH") info.blendMode = node.blendMode; } catch(e) {}
|
|
1133
1440
|
try { if ("clipsContent" in node && node.clipsContent) info.clipsContent = true; } catch(e) {}
|
|
1134
1441
|
|
|
1135
|
-
// ── Bound Variables (Design Tokens) — full
|
|
1136
|
-
if (isFull) try {
|
|
1442
|
+
// ── Bound Variables (Design Tokens) — full & compact ──
|
|
1443
|
+
if (isFull || isCompact) try {
|
|
1137
1444
|
if (node.boundVariables) {
|
|
1138
1445
|
var bv = {};
|
|
1139
1446
|
var bvKeys = Object.keys(node.boundVariables);
|
|
1447
|
+
var varMap = (walkState && walkState.variableMap) ? walkState.variableMap.byId : null;
|
|
1140
1448
|
for (var bvi = 0; bvi < bvKeys.length; bvi++) {
|
|
1141
1449
|
var bvk = bvKeys[bvi];
|
|
1142
1450
|
var binding = node.boundVariables[bvk];
|
|
1143
1451
|
if (binding) {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1452
|
+
var bindings = Array.isArray(binding) ? binding : [binding];
|
|
1453
|
+
var resolvedList = [];
|
|
1454
|
+
for (var bi = 0; bi < bindings.length; bi++) {
|
|
1455
|
+
var bObj = bindings[bi];
|
|
1456
|
+
if (!bObj || !bObj.id) continue;
|
|
1457
|
+
var vInfo = varMap && varMap[bObj.id] ? varMap[bObj.id] : null;
|
|
1458
|
+
if (vInfo) {
|
|
1459
|
+
var entry = { id: bObj.id, name: vInfo.name, type: vInfo.resolvedType };
|
|
1460
|
+
if (vInfo.resolvedValue !== null && vInfo.resolvedValue !== undefined) entry.value = vInfo.resolvedValue;
|
|
1461
|
+
if (vInfo.isAlias && vInfo.targetName) entry.aliasTarget = vInfo.targetName;
|
|
1462
|
+
resolvedList.push(entry);
|
|
1463
|
+
} else {
|
|
1464
|
+
resolvedList.push({ id: bObj.id });
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (resolvedList.length > 0) {
|
|
1468
|
+
bv[bvk] = Array.isArray(binding) ? resolvedList : resolvedList[0];
|
|
1469
|
+
var primaryEntry = resolvedList[0];
|
|
1470
|
+
if (primaryEntry && primaryEntry.name) {
|
|
1471
|
+
if (bvk === "fills" || bvk === "fill") info.fillToken = primaryEntry.name;
|
|
1472
|
+
else if (bvk === "strokes" || bvk === "stroke") info.strokeToken = primaryEntry.name;
|
|
1473
|
+
else if (bvk === "itemSpacing") info.gapToken = primaryEntry.name;
|
|
1474
|
+
else if (bvk === "paddingTop" || bvk === "paddingBottom" || bvk === "paddingLeft" || bvk === "paddingRight") info.paddingToken = primaryEntry.name;
|
|
1475
|
+
else if (bvk === "topLeftRadius" || bvk === "cornerRadius") info.radiusToken = primaryEntry.name;
|
|
1476
|
+
}
|
|
1148
1477
|
}
|
|
1149
1478
|
}
|
|
1150
1479
|
}
|
|
@@ -1210,6 +1539,8 @@ function extractDesignTree(node, depth, maxDepth, detailLevel, filterInvisible,
|
|
|
1210
1539
|
mode: node.layoutMode,
|
|
1211
1540
|
};
|
|
1212
1541
|
if (node.itemSpacing !== undefined && node.itemSpacing !== 0) layoutObj.itemSpacing = node.itemSpacing;
|
|
1542
|
+
if (info.gapToken) layoutObj.gapToken = info.gapToken;
|
|
1543
|
+
if (info.paddingToken) layoutObj.paddingToken = info.paddingToken;
|
|
1213
1544
|
if (node.primaryAxisAlignItems && node.primaryAxisAlignItems !== "MIN") layoutObj.align = node.primaryAxisAlignItems;
|
|
1214
1545
|
if (node.counterAxisAlignItems && node.counterAxisAlignItems !== "MIN") layoutObj.crossAlign = node.counterAxisAlignItems;
|
|
1215
1546
|
|
|
@@ -1259,7 +1590,6 @@ function extractDesignTree(node, depth, maxDepth, detailLevel, filterInvisible,
|
|
|
1259
1590
|
// ── Component-specific info ──
|
|
1260
1591
|
if (node.type === "COMPONENT" || node.type === "COMPONENT_SET") {
|
|
1261
1592
|
try { info.description = node.description; } catch(e) {}
|
|
1262
|
-
// Expose component property definitions for COMPONENT/COMPONENT_SET
|
|
1263
1593
|
try {
|
|
1264
1594
|
if (node.componentPropertyDefinitions) {
|
|
1265
1595
|
var defs = node.componentPropertyDefinitions;
|
|
@@ -1276,27 +1606,17 @@ function extractDesignTree(node, depth, maxDepth, detailLevel, filterInvisible,
|
|
|
1276
1606
|
} catch(e) {}
|
|
1277
1607
|
}
|
|
1278
1608
|
if (node.type === "INSTANCE") {
|
|
1279
|
-
|
|
1280
|
-
|
|
1609
|
+
var parsedProps = cleanComponentProperties(node);
|
|
1610
|
+
if (parsedProps.variant) info.variant = parsedProps.variant;
|
|
1611
|
+
if (parsedProps.props) info.props = parsedProps.props;
|
|
1281
1612
|
if (instanceCollector) instanceCollector.push({ info: info, node: node });
|
|
1282
1613
|
try { if (node.overrides && node.overrides.length) info.overrideCount = node.overrides.length; } catch(e) {}
|
|
1283
|
-
// Issue #4: expose explicit component property values on this instance
|
|
1284
|
-
try {
|
|
1285
|
-
if (node.componentProperties) {
|
|
1286
|
-
var props = node.componentProperties;
|
|
1287
|
-
var propKeys = Object.keys(props);
|
|
1288
|
-
if (propKeys.length > 0) {
|
|
1289
|
-
info.componentPropertyValues = {};
|
|
1290
|
-
for (var pi = 0; pi < propKeys.length; pi++) {
|
|
1291
|
-
var pk = propKeys[pi];
|
|
1292
|
-
var pv = props[pk];
|
|
1293
|
-
info.componentPropertyValues[pk] = { type: pv.type, value: pv.value };
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
} catch(e) {}
|
|
1298
1614
|
}
|
|
1299
1615
|
|
|
1616
|
+
// ── Semantic Role ──
|
|
1617
|
+
var semanticRole = inferSemanticRole(node, info);
|
|
1618
|
+
if (semanticRole) info.role = semanticRole;
|
|
1619
|
+
|
|
1300
1620
|
// ── VECTOR / BOOLEAN_OPERATION ──
|
|
1301
1621
|
if (node.type === "VECTOR" || node.type === "BOOLEAN_OPERATION") {
|
|
1302
1622
|
try { if (node.vectorPaths) info.pathCount = node.vectorPaths.length; } catch(e) {}
|
|
@@ -1348,7 +1668,7 @@ const handlers = {};
|
|
|
1348
1668
|
|
|
1349
1669
|
handlers.status = async () => ({
|
|
1350
1670
|
connected: true,
|
|
1351
|
-
version: "3.0.
|
|
1671
|
+
version: "3.0.4",
|
|
1352
1672
|
fileName: figma.root.name,
|
|
1353
1673
|
currentPage: figma.currentPage.name,
|
|
1354
1674
|
pageCount: figma.root.children.length,
|
|
@@ -1414,13 +1734,15 @@ handlers.createPage = async (params) => {
|
|
|
1414
1734
|
};
|
|
1415
1735
|
|
|
1416
1736
|
handlers.query = async (params) => {
|
|
1417
|
-
|
|
1737
|
+
params = params || {};
|
|
1738
|
+
var type = params.type;
|
|
1739
|
+
var name = params.name || params.nodeName || params.query || params.q;
|
|
1740
|
+
var id = params.id || params.nodeId;
|
|
1418
1741
|
var parentId = params.parentId, limit = params.limit || 500;
|
|
1419
1742
|
if (id) {
|
|
1420
1743
|
const n = await findNodeByIdAsync(id);
|
|
1421
1744
|
return n ? [nodeToInfo(n)] : [];
|
|
1422
1745
|
}
|
|
1423
|
-
if (!type && !name) throw new Error("query requires at least one of: type, name, id");
|
|
1424
1746
|
// BUG-03: parentId filter scopes search to a subtree; default cap raised 100 → 500.
|
|
1425
1747
|
var scope = figma.currentPage;
|
|
1426
1748
|
if (parentId) {
|
|
@@ -1429,10 +1751,14 @@ handlers.query = async (params) => {
|
|
|
1429
1751
|
if (typeof parent.findAll !== "function") return [];
|
|
1430
1752
|
scope = parent;
|
|
1431
1753
|
}
|
|
1754
|
+
if (!type && !name) {
|
|
1755
|
+
var allChildren = scope.children || [];
|
|
1756
|
+
return allChildren.slice(0, limit).map(nodeToInfo);
|
|
1757
|
+
}
|
|
1432
1758
|
const results = scope.findAll(n => {
|
|
1433
|
-
if (type && name) return n.type === type && n.name === name;
|
|
1759
|
+
if (type && name) return n.type === type && (n.name === name || (typeof name === "string" && n.name.toLowerCase().includes(name.toLowerCase())));
|
|
1434
1760
|
if (type) return n.type === type;
|
|
1435
|
-
return n.name === name;
|
|
1761
|
+
return n.name === name || (typeof name === "string" && n.name.toLowerCase().includes(name.toLowerCase()));
|
|
1436
1762
|
});
|
|
1437
1763
|
return results.slice(0, limit).map(nodeToInfo);
|
|
1438
1764
|
};
|
|
@@ -2336,6 +2662,21 @@ handlers.swapComponent = async function(params) {
|
|
|
2336
2662
|
};
|
|
2337
2663
|
};
|
|
2338
2664
|
|
|
2665
|
+
// ─── ALIASES FOR WRITE HANDLERS ──────────────────────────────────────────────
|
|
2666
|
+
handlers.list_pages = handlers.listPages;
|
|
2667
|
+
handlers.set_page = handlers.setPage;
|
|
2668
|
+
handlers.create_page = handlers.createPage;
|
|
2669
|
+
handlers.list_components = handlers.listComponents;
|
|
2670
|
+
handlers.load_all_pages_async = handlers.loadAllPagesAsync;
|
|
2671
|
+
handlers.loadAllPages = handlers.loadAllPagesAsync;
|
|
2672
|
+
handlers.load_all_pages = handlers.loadAllPagesAsync;
|
|
2673
|
+
handlers.set_component_properties = handlers.setComponentProperties;
|
|
2674
|
+
handlers.get_component_properties = handlers.getComponentProperties;
|
|
2675
|
+
handlers.swap_component = handlers.swapComponent;
|
|
2676
|
+
handlers.update = handlers.modify;
|
|
2677
|
+
handlers.remove = handlers["delete"];
|
|
2678
|
+
|
|
2679
|
+
|
|
2339
2680
|
|
|
2340
2681
|
// ── plugin-src/handlers-library.js ──
|
|
2341
2682
|
// ─── DESIGN LIBRARY HANDLERS ──────────────────────────────────────────────────
|
|
@@ -2466,6 +2807,11 @@ handlers.get_library_tokens = async function() {
|
|
|
2466
2807
|
return { libraryId: lib.id, colors: colors, textStyles: textStyles };
|
|
2467
2808
|
};
|
|
2468
2809
|
|
|
2810
|
+
// ─── ALIASES FOR LIBRARY HANDLERS ─────────────────────────────────────────────
|
|
2811
|
+
handlers.ensureLibrary = handlers.ensure_library;
|
|
2812
|
+
handlers.getLibraryTokens = handlers.get_library_tokens;
|
|
2813
|
+
|
|
2814
|
+
|
|
2469
2815
|
|
|
2470
2816
|
// ── plugin-src/handlers-read.js ──
|
|
2471
2817
|
// ─── READ HANDLERS ────────────────────────────────────────────────────────────
|
|
@@ -2476,10 +2822,16 @@ handlers.get_library_tokens = async function() {
|
|
|
2476
2822
|
// `absolute: true` forces absoluteBoundingBox on every node.
|
|
2477
2823
|
var DEFAULT_NODE_BUDGET = 3000;
|
|
2478
2824
|
|
|
2479
|
-
function
|
|
2825
|
+
async function makeWalkStateAsync(params) {
|
|
2480
2826
|
var p = params || {};
|
|
2481
2827
|
var budget = (p.maxNodes !== undefined && Number(p.maxNodes) > 0) ? Number(p.maxNodes) : DEFAULT_NODE_BUDGET;
|
|
2482
|
-
|
|
2828
|
+
var varMap = null;
|
|
2829
|
+
try {
|
|
2830
|
+
if (typeof buildVariableResolverMapAsync === "function") {
|
|
2831
|
+
varMap = await buildVariableResolverMapAsync();
|
|
2832
|
+
}
|
|
2833
|
+
} catch(e) {}
|
|
2834
|
+
return { remaining: budget, budget: budget, truncated: false, absolute: p.absolute === true, variableMap: varMap };
|
|
2483
2835
|
}
|
|
2484
2836
|
|
|
2485
2837
|
function walkStateMeta(walkState) {
|
|
@@ -2512,7 +2864,7 @@ handlers.get_selection = async function(params) {
|
|
|
2512
2864
|
var filterInvisible = !(params && params.includeHidden === true);
|
|
2513
2865
|
var tokenCollector = (detailLevel !== "minimal") ? { colors: new Set(), fonts: new Set(), sizes: new Set() } : null;
|
|
2514
2866
|
var instanceCollector = (detailLevel !== "minimal") ? [] : null;
|
|
2515
|
-
var walkState =
|
|
2867
|
+
var walkState = await makeWalkStateAsync(params);
|
|
2516
2868
|
var trees = nodes.map(function(n) { return extractDesignTree(n, 0, maxDepth, detailLevel, filterInvisible, tokenCollector, instanceCollector, walkState); });
|
|
2517
2869
|
// mainComponent is async-only under documentAccess: dynamic-page — resolve
|
|
2518
2870
|
// every collected INSTANCE after the (synchronous) tree walk.
|
|
@@ -2553,7 +2905,7 @@ handlers.get_design = async function(params) {
|
|
|
2553
2905
|
try {
|
|
2554
2906
|
var tokenCollector = (detailLevel !== "minimal") ? { colors: new Set(), fonts: new Set(), sizes: new Set() } : null;
|
|
2555
2907
|
var instanceCollector = (detailLevel !== "minimal") ? [] : null;
|
|
2556
|
-
var walkState =
|
|
2908
|
+
var walkState = await makeWalkStateAsync(p);
|
|
2557
2909
|
var tree = extractDesignTree(root, 0, maxDepth, detailLevel, filterInvisible, tokenCollector, instanceCollector, walkState);
|
|
2558
2910
|
var instanceInfo = await resolveInstanceComponents(instanceCollector);
|
|
2559
2911
|
|
|
@@ -3129,23 +3481,26 @@ async function exportNodeSvg(node) {
|
|
|
3129
3481
|
|
|
3130
3482
|
// export_svg — export node as SVG string
|
|
3131
3483
|
handlers.export_svg = async function(params) {
|
|
3132
|
-
var id = params ? params.id : null;
|
|
3133
|
-
var nodeName = params ? params.name : null;
|
|
3484
|
+
var id = params ? (params.id || params.nodeId) : null;
|
|
3485
|
+
var nodeName = params ? (params.name || params.nodeName) : null;
|
|
3134
3486
|
var node = null;
|
|
3135
3487
|
if (id) node = await findNodeByIdAsync(id);
|
|
3136
3488
|
if (!node && nodeName) {
|
|
3137
3489
|
node = findNodeByName(nodeName);
|
|
3138
3490
|
}
|
|
3491
|
+
if (!node && figma.currentPage.selection.length > 0) {
|
|
3492
|
+
node = figma.currentPage.selection[0];
|
|
3493
|
+
}
|
|
3139
3494
|
if (!node) node = figma.currentPage;
|
|
3140
|
-
if (!node) throw new Error("Node not found");
|
|
3495
|
+
if (!node) throw new Error("Node not found for export");
|
|
3141
3496
|
var svg = await exportNodeSvg(node);
|
|
3142
3497
|
return { svg: svg, nodeId: node.id, width: Math.round(node.width), height: Math.round(node.height) };
|
|
3143
3498
|
};
|
|
3144
3499
|
|
|
3145
3500
|
// export_image — export node as base64 PNG/JPG (for saving to disk, not for inline display)
|
|
3146
3501
|
handlers.export_image = async function(params) {
|
|
3147
|
-
var id = params ? params.id : null;
|
|
3148
|
-
var nodeName = params ? params.name : null;
|
|
3502
|
+
var id = params ? (params.id || params.nodeId) : null;
|
|
3503
|
+
var nodeName = params ? (params.name || params.nodeName) : null;
|
|
3149
3504
|
var format = (params && params.format) ? params.format.toUpperCase() : "PNG";
|
|
3150
3505
|
var scale = (params && params.scale) ? params.scale : 2;
|
|
3151
3506
|
|
|
@@ -3156,6 +3511,16 @@ handlers.export_image = async function(params) {
|
|
|
3156
3511
|
if (!node && nodeName) {
|
|
3157
3512
|
node = findNodeByName(nodeName);
|
|
3158
3513
|
}
|
|
3514
|
+
if (!node && figma.currentPage.selection.length > 0) {
|
|
3515
|
+
node = figma.currentPage.selection[0];
|
|
3516
|
+
}
|
|
3517
|
+
if (!node) {
|
|
3518
|
+
var children = figma.currentPage.children || [];
|
|
3519
|
+
var exportableTypes = ["FRAME", "COMPONENT", "COMPONENT_SET", "SECTION", "INSTANCE", "GROUP"];
|
|
3520
|
+
for (var i = 0; i < children.length; i++) {
|
|
3521
|
+
if (exportableTypes.indexOf(children[i].type) !== -1) { node = children[i]; break; }
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3159
3524
|
if (!node) throw new Error("Node not found for export");
|
|
3160
3525
|
|
|
3161
3526
|
// BUG-05 fix: same as screenshot — force render before export, then put the
|
|
@@ -3287,19 +3652,54 @@ handlers.index_scan = async function() {
|
|
|
3287
3652
|
};
|
|
3288
3653
|
};
|
|
3289
3654
|
|
|
3655
|
+
// ─── ALIASES FOR READ HANDLERS ────────────────────────────────────────────────
|
|
3656
|
+
handlers.getSelection = handlers.get_selection;
|
|
3657
|
+
handlers.selection = handlers.get_selection;
|
|
3658
|
+
handlers.getDesign = handlers.get_design;
|
|
3659
|
+
handlers.scanDesign = handlers.scan_design;
|
|
3660
|
+
handlers.searchNodes = handlers.search_nodes;
|
|
3661
|
+
handlers.getPageNodes = handlers.get_page_nodes;
|
|
3662
|
+
handlers.pageNodes = handlers.get_page_nodes;
|
|
3663
|
+
handlers.capture = handlers.screenshot;
|
|
3664
|
+
handlers.takeScreenshot = handlers.screenshot;
|
|
3665
|
+
handlers.exportSvg = handlers.export_svg;
|
|
3666
|
+
handlers.exportImage = handlers.export_image;
|
|
3667
|
+
handlers.indexScan = handlers.index_scan;
|
|
3668
|
+
|
|
3669
|
+
|
|
3290
3670
|
|
|
3291
3671
|
// ── plugin-src/handlers-read-detail.js ──
|
|
3292
3672
|
// ─── READ DETAIL HANDLERS ─────────────────────────────────────────────────────
|
|
3293
3673
|
|
|
3294
3674
|
// get_node_detail — CSS-like properties for a single node (no tree traversal)
|
|
3295
3675
|
handlers.get_node_detail = async function(params) {
|
|
3296
|
-
//
|
|
3297
|
-
|
|
3298
|
-
|
|
3676
|
+
// Batch support: if nodeIds array is provided, inspect multiple nodes in one call
|
|
3677
|
+
if (params && (Array.isArray(params.nodeIds) || Array.isArray(params.ids))) {
|
|
3678
|
+
var ids = params.nodeIds || params.ids;
|
|
3679
|
+
var results = [];
|
|
3680
|
+
for (var bi = 0; bi < ids.length; bi++) {
|
|
3681
|
+
try {
|
|
3682
|
+
var nd = await handlers.get_node_detail({ id: ids[bi] });
|
|
3683
|
+
results.push(nd);
|
|
3684
|
+
} catch (e) {
|
|
3685
|
+
results.push({ id: ids[bi], error: e.message });
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
return { nodes: results, count: results.length };
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
// Accept id, nodeId, node_id, targetId, target_id, name, nodeName, node_name — try ID first then name fallback
|
|
3692
|
+
var id = params ? (params.id || params.nodeId || params.node_id || params.targetId || params.target_id) : null;
|
|
3693
|
+
var nodeName = params ? (params.name || params.nodeName || params.node_name) : null;
|
|
3299
3694
|
var node = null;
|
|
3300
3695
|
if (id) node = await findNodeByIdAsync(id);
|
|
3301
3696
|
if (!node && nodeName) node = findNodeByName(nodeName);
|
|
3302
|
-
if (!node)
|
|
3697
|
+
if (!node) {
|
|
3698
|
+
var errMsg = typeof getNodeNotFoundContext === "function"
|
|
3699
|
+
? getNodeNotFoundContext(id, nodeName)
|
|
3700
|
+
: ("Node not found: " + (id || nodeName || "no id/name given") + ". Use figma_read get_page_nodes to get current node IDs.");
|
|
3701
|
+
throw new Error(errMsg);
|
|
3702
|
+
}
|
|
3303
3703
|
|
|
3304
3704
|
var detail = {
|
|
3305
3705
|
id: node.id, name: node.name, type: node.type,
|
|
@@ -3531,6 +3931,25 @@ handlers.get_node_detail = async function(params) {
|
|
|
3531
3931
|
}
|
|
3532
3932
|
} catch(e) {}
|
|
3533
3933
|
|
|
3934
|
+
// Attach direct semantic token names on detail
|
|
3935
|
+
try {
|
|
3936
|
+
if (detail.boundVariables) {
|
|
3937
|
+
var bvKeys = Object.keys(detail.boundVariables);
|
|
3938
|
+
for (var bvi = 0; bvi < bvKeys.length; bvi++) {
|
|
3939
|
+
var bvk = bvKeys[bvi];
|
|
3940
|
+
var bEntry = detail.boundVariables[bvk];
|
|
3941
|
+
var pName = (bEntry && typeof bEntry === "object") ? (Array.isArray(bEntry) ? (bEntry[0] && bEntry[0].name) : bEntry.name) : null;
|
|
3942
|
+
if (pName) {
|
|
3943
|
+
if (bvk === "fills" || bvk === "fill") detail.fillToken = pName;
|
|
3944
|
+
else if (bvk === "strokes" || bvk === "stroke") detail.strokeToken = pName;
|
|
3945
|
+
else if (bvk === "itemSpacing") detail.gapToken = pName;
|
|
3946
|
+
else if (bvk === "paddingTop" || bvk === "paddingBottom" || bvk === "paddingLeft" || bvk === "paddingRight") detail.paddingToken = pName;
|
|
3947
|
+
else if (bvk === "topLeftRadius" || bvk === "cornerRadius") detail.radiusToken = pName;
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
} catch(e) {}
|
|
3952
|
+
|
|
3534
3953
|
// P4: Resolve style IDs → name + values (not just opaque IDs)
|
|
3535
3954
|
try {
|
|
3536
3955
|
if (node.textStyleId && typeof node.textStyleId === "string") {
|
|
@@ -3557,12 +3976,15 @@ handlers.get_node_detail = async function(params) {
|
|
|
3557
3976
|
try { if (node.strokeStyleId && typeof node.strokeStyleId === "string") detail.strokeStyleId = node.strokeStyleId; } catch(e) {}
|
|
3558
3977
|
try { if (node.effectStyleId && typeof node.effectStyleId === "string") detail.effectStyleId = node.effectStyleId; } catch(e) {}
|
|
3559
3978
|
|
|
3560
|
-
// Instance: source component reference + property values + overrides + P5 variantLabel
|
|
3979
|
+
// Instance: source component reference + property values + overrides + P5 variantLabel + clean variant & props
|
|
3561
3980
|
if (node.type === "INSTANCE") {
|
|
3562
|
-
// mainComponent is async-only under documentAccess: dynamic-page.
|
|
3563
|
-
// describeMainComponent yields componentId/componentName/componentSetName/variantLabel.
|
|
3564
3981
|
var instComp = await getMainComponentSafe(node);
|
|
3565
3982
|
if (instComp) Object.assign(detail, describeMainComponent(instComp));
|
|
3983
|
+
var parsed = typeof cleanComponentProperties === "function" ? cleanComponentProperties(node) : null;
|
|
3984
|
+
if (parsed) {
|
|
3985
|
+
if (parsed.variant) detail.variant = parsed.variant;
|
|
3986
|
+
if (parsed.props) detail.props = parsed.props;
|
|
3987
|
+
}
|
|
3566
3988
|
// P3: Full override list instead of just count
|
|
3567
3989
|
try {
|
|
3568
3990
|
if (node.overrides && node.overrides.length) {
|
|
@@ -3588,6 +4010,10 @@ handlers.get_node_detail = async function(params) {
|
|
|
3588
4010
|
} catch(e) {}
|
|
3589
4011
|
}
|
|
3590
4012
|
|
|
4013
|
+
// Semantic Role
|
|
4014
|
+
var semanticRole = typeof inferSemanticRole === "function" ? inferSemanticRole(node, detail) : null;
|
|
4015
|
+
if (semanticRole) detail.role = semanticRole;
|
|
4016
|
+
|
|
3591
4017
|
// Children count + text content summary
|
|
3592
4018
|
if (node && typeof node === "object" && "children" in node && Array.isArray(node.children)) {
|
|
3593
4019
|
detail.childCount = node.children.length;
|
|
@@ -3695,8 +4121,8 @@ handlers.get_css = async function(params) {
|
|
|
3695
4121
|
// Returns: flex layout semantics, token-resolved colors, typography, component instances
|
|
3696
4122
|
// Much more code-ready than get_design (raw tree) or get_css (single string)
|
|
3697
4123
|
handlers.get_design_context = async function(params) {
|
|
3698
|
-
var id = params ? (params.id || params.nodeId) : null;
|
|
3699
|
-
var nodeName = params ? (params.name || params.nodeName) : null;
|
|
4124
|
+
var id = params ? (params.id || params.nodeId || params.node_id || params.targetId || params.target_id) : null;
|
|
4125
|
+
var nodeName = params ? (params.name || params.nodeName || params.node_name) : null;
|
|
3700
4126
|
var node = null;
|
|
3701
4127
|
if (id) node = await findNodeByIdAsync(id);
|
|
3702
4128
|
if (!node && nodeName) node = findNodeByName(nodeName);
|
|
@@ -3705,7 +4131,12 @@ handlers.get_design_context = async function(params) {
|
|
|
3705
4131
|
var sel = figma.currentPage.selection;
|
|
3706
4132
|
node = sel && sel.length > 0 ? sel[0] : null;
|
|
3707
4133
|
}
|
|
3708
|
-
if (!node)
|
|
4134
|
+
if (!node) {
|
|
4135
|
+
var errMsg = (id || nodeName) && typeof getNodeNotFoundContext === "function"
|
|
4136
|
+
? getNodeNotFoundContext(id, nodeName)
|
|
4137
|
+
: "No node specified and nothing selected. Pass nodeId or select a node in Figma.";
|
|
4138
|
+
throw new Error(errMsg);
|
|
4139
|
+
}
|
|
3709
4140
|
|
|
3710
4141
|
// Build variable name lookup: variableId → name
|
|
3711
4142
|
// Use getLocalVariablesAsync (single call) instead of per-ID fetches — avoids O(n²) await loop
|
|
@@ -3770,6 +4201,26 @@ handlers.get_design_context = async function(params) {
|
|
|
3770
4201
|
var fillVal = resolveFill(nd);
|
|
3771
4202
|
if (fillVal) ctx.fill = fillVal;
|
|
3772
4203
|
|
|
4204
|
+
// Direct token bindings
|
|
4205
|
+
try {
|
|
4206
|
+
if (nd.boundVariables) {
|
|
4207
|
+
var bvKeys = Object.keys(nd.boundVariables);
|
|
4208
|
+
for (var bvi = 0; bvi < bvKeys.length; bvi++) {
|
|
4209
|
+
var bvk = bvKeys[bvi];
|
|
4210
|
+
var binding = nd.boundVariables[bvk];
|
|
4211
|
+
var bindId = binding ? (Array.isArray(binding) ? (binding[0] ? binding[0].id : null) : binding.id) : null;
|
|
4212
|
+
if (bindId && varMap[bindId]) {
|
|
4213
|
+
var vName = varMap[bindId].name;
|
|
4214
|
+
if (bvk === "fills" || bvk === "fill") ctx.fillToken = vName;
|
|
4215
|
+
else if (bvk === "strokes" || bvk === "stroke") ctx.strokeToken = vName;
|
|
4216
|
+
else if (bvk === "itemSpacing") ctx.gapToken = vName;
|
|
4217
|
+
else if (bvk === "paddingTop" || bvk === "paddingBottom" || bvk === "paddingLeft" || bvk === "paddingRight") ctx.paddingToken = vName;
|
|
4218
|
+
else if (bvk === "topLeftRadius" || bvk === "cornerRadius") ctx.radiusToken = vName;
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
} catch(e) {}
|
|
4223
|
+
|
|
3773
4224
|
// Stroke
|
|
3774
4225
|
try {
|
|
3775
4226
|
if (nd.strokes && nd.strokes.length && nd.strokes[0].type === "SOLID") {
|
|
@@ -3822,16 +4273,17 @@ handlers.get_design_context = async function(params) {
|
|
|
3822
4273
|
// documentAccess: dynamic-page, so collect now and resolve after the walk.
|
|
3823
4274
|
if (nd.type === "INSTANCE") {
|
|
3824
4275
|
ctxInstances.push({ info: ctx, node: nd });
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
if (pks.length) ctx.component = { properties: props };
|
|
3831
|
-
}
|
|
3832
|
-
} catch(e) {}
|
|
4276
|
+
var parsedProps = typeof cleanComponentProperties === "function" ? cleanComponentProperties(nd) : null;
|
|
4277
|
+
if (parsedProps) {
|
|
4278
|
+
if (parsedProps.variant) ctx.variant = parsedProps.variant;
|
|
4279
|
+
if (parsedProps.props) ctx.props = parsedProps.props;
|
|
4280
|
+
}
|
|
3833
4281
|
}
|
|
3834
4282
|
|
|
4283
|
+
// Role
|
|
4284
|
+
var sRole = typeof inferSemanticRole === "function" ? inferSemanticRole(nd, ctx) : null;
|
|
4285
|
+
if (sRole) ctx.role = sRole;
|
|
4286
|
+
|
|
3835
4287
|
// Children (limited depth to avoid token overflow)
|
|
3836
4288
|
if (depth < 4 && nd.children && nd.children.length) {
|
|
3837
4289
|
ctx.children = [];
|
|
@@ -3888,9 +4340,23 @@ handlers.get_design_context = async function(params) {
|
|
|
3888
4340
|
|
|
3889
4341
|
// Aliases for developer convenience & MCP tool naming compatibility
|
|
3890
4342
|
handlers.inspect_node = handlers.get_design_context;
|
|
4343
|
+
handlers.inspectNode = handlers.get_design_context;
|
|
3891
4344
|
handlers.inspect = handlers.get_design_context;
|
|
4345
|
+
handlers.getDesignContext = handlers.get_design_context;
|
|
4346
|
+
handlers.designContext = handlers.get_design_context;
|
|
4347
|
+
handlers.getCss = handlers.get_css;
|
|
4348
|
+
handlers.css = handlers.get_css;
|
|
4349
|
+
handlers.get_node = handlers.get_node_detail;
|
|
4350
|
+
handlers.getNode = handlers.get_node_detail;
|
|
4351
|
+
handlers.get_node_detail = handlers.get_node_detail;
|
|
4352
|
+
handlers.getNodeDetail = handlers.get_node_detail;
|
|
3892
4353
|
handlers.get_node_info = handlers.get_node_detail;
|
|
4354
|
+
handlers.getNodeInfo = handlers.get_node_detail;
|
|
3893
4355
|
handlers.node_detail = handlers.get_node_detail;
|
|
4356
|
+
handlers.nodeDetail = handlers.get_node_detail;
|
|
4357
|
+
handlers.node_info = handlers.get_node_detail;
|
|
4358
|
+
handlers.nodeInfo = handlers.get_node_detail;
|
|
4359
|
+
|
|
3894
4360
|
|
|
3895
4361
|
// get_component_map — list every component instance in a frame with variant properties
|
|
3896
4362
|
// Use this to map Figma components to their code equivalents
|
|
@@ -4200,8 +4666,22 @@ handlers.get_variables = async function() {
|
|
|
4200
4666
|
return { collections: collections, resolvedTokens: resolvedTokensMap };
|
|
4201
4667
|
};
|
|
4202
4668
|
|
|
4203
|
-
handlers.
|
|
4204
|
-
handlers.
|
|
4669
|
+
handlers.get_variable_tokens = handlers.get_variables;
|
|
4670
|
+
handlers.getVariableTokens = handlers.get_variables;
|
|
4671
|
+
handlers.get_tokens = handlers.get_variables;
|
|
4672
|
+
handlers.getTokens = handlers.get_variables;
|
|
4673
|
+
handlers.tokens = handlers.get_variables;
|
|
4674
|
+
handlers.variables = handlers.get_variables;
|
|
4675
|
+
handlers.getVariables = handlers.get_variables;
|
|
4676
|
+
handlers.getStyles = handlers.get_styles;
|
|
4677
|
+
handlers.styles = handlers.get_styles;
|
|
4678
|
+
handlers.getLocalComponents = handlers.get_local_components;
|
|
4679
|
+
handlers.localComponents = handlers.get_local_components;
|
|
4680
|
+
handlers.getViewport = handlers.get_viewport;
|
|
4681
|
+
handlers.setViewport = handlers.set_viewport;
|
|
4682
|
+
handlers.getComponentMap = handlers.get_component_map;
|
|
4683
|
+
handlers.getUnmappedComponents = handlers.get_unmapped_components;
|
|
4684
|
+
|
|
4205
4685
|
|
|
4206
4686
|
// ─── EXPORT ASSETS (Batch Icon & Image Extractor) ────────────────────────────
|
|
4207
4687
|
handlers.export_assets = async function(params) {
|
|
@@ -4328,6 +4808,9 @@ handlers.export_assets = async function(params) {
|
|
|
4328
4808
|
};
|
|
4329
4809
|
};
|
|
4330
4810
|
|
|
4811
|
+
handlers.exportAssets = handlers.export_assets;
|
|
4812
|
+
|
|
4813
|
+
|
|
4331
4814
|
|
|
4332
4815
|
// ── plugin-src/handlers-tokens.js ──
|
|
4333
4816
|
// ─── DESIGN TOKEN OPERATIONS (v1.7.0) ───────────────────────────────────────
|
|
@@ -5451,6 +5934,29 @@ handlers.removeComponentProperty = async function(params) {
|
|
|
5451
5934
|
};
|
|
5452
5935
|
};
|
|
5453
5936
|
|
|
5937
|
+
// ─── ALIASES FOR TOKEN & COMPONENT PROPERTY HANDLERS ─────────────────────────
|
|
5938
|
+
handlers.create_variable_collection = handlers.createVariableCollection;
|
|
5939
|
+
handlers.create_variable = handlers.createVariable;
|
|
5940
|
+
handlers.add_variable_mode = handlers.addVariableMode;
|
|
5941
|
+
handlers.rename_variable_mode = handlers.renameVariableMode;
|
|
5942
|
+
handlers.remove_variable_mode = handlers.removeVariableMode;
|
|
5943
|
+
handlers.set_variable_value = handlers.setVariableValue;
|
|
5944
|
+
handlers.set_frame_variable_mode = handlers.setFrameVariableMode;
|
|
5945
|
+
handlers.clear_frame_variable_mode = handlers.clearFrameVariableMode;
|
|
5946
|
+
handlers.apply_variable = handlers.applyVariable;
|
|
5947
|
+
handlers.create_paint_style = handlers.createPaintStyle;
|
|
5948
|
+
handlers.create_text_style = handlers.createTextStyle;
|
|
5949
|
+
handlers.create_component = handlers.createComponent;
|
|
5950
|
+
handlers.modify_variable = handlers.modifyVariable;
|
|
5951
|
+
handlers.setup_design_tokens = handlers.setupDesignTokens;
|
|
5952
|
+
handlers.apply_text_style = handlers.applyTextStyle;
|
|
5953
|
+
handlers.add_component_property = handlers.addComponentProperty;
|
|
5954
|
+
handlers.bind_component_property_to_text = handlers.bindComponentPropertyToText;
|
|
5955
|
+
handlers.bind_component_property = handlers.bindComponentProperty;
|
|
5956
|
+
handlers.unbind_component_property = handlers.unbindComponentProperty;
|
|
5957
|
+
handlers.remove_component_property = handlers.removeComponentProperty;
|
|
5958
|
+
|
|
5959
|
+
|
|
5454
5960
|
|
|
5455
5961
|
// ── plugin-src/handlers-write-ops.js ──
|
|
5456
5962
|
// ─── NEW WRITE OPERATIONS ────────────────────────────────────────────────────
|
|
@@ -5548,6 +6054,8 @@ handlers.set_selection = async function(params) {
|
|
|
5548
6054
|
return { selected: nodes.map(nodeToInfo) };
|
|
5549
6055
|
};
|
|
5550
6056
|
|
|
6057
|
+
handlers.setSelection = handlers.set_selection;
|
|
6058
|
+
|
|
5551
6059
|
// batch — execute multiple operations in one call
|
|
5552
6060
|
// Supported operations: create, modify, delete (single or batch ids), append, clone
|
|
5553
6061
|
handlers.batch = async function(params) {
|
|
@@ -5559,7 +6067,7 @@ handlers.batch = async function(params) {
|
|
|
5559
6067
|
var results = [];
|
|
5560
6068
|
for (var i = 0; i < operations.length; i++) {
|
|
5561
6069
|
var op = operations[i];
|
|
5562
|
-
var handler = handlers[op.operation];
|
|
6070
|
+
var handler = typeof resolveOperationHandler === "function" ? resolveOperationHandler(op.operation) : handlers[op.operation];
|
|
5563
6071
|
if (!handler) {
|
|
5564
6072
|
results.push({ index: i, operation: op.operation, success: false, error: "Unknown operation" });
|
|
5565
6073
|
continue;
|
|
@@ -5575,6 +6083,7 @@ handlers.batch = async function(params) {
|
|
|
5575
6083
|
};
|
|
5576
6084
|
|
|
5577
6085
|
|
|
6086
|
+
|
|
5578
6087
|
// ── plugin-src/main.js ──
|
|
5579
6088
|
// ─── PLUGIN ENTRY POINT ───────────────────────────────────────────────────────
|
|
5580
6089
|
|
|
@@ -5901,7 +6410,9 @@ figma.ui.onmessage = async (request) => {
|
|
|
5901
6410
|
}
|
|
5902
6411
|
|
|
5903
6412
|
const { id, operation, params } = request;
|
|
5904
|
-
const handler =
|
|
6413
|
+
const handler = typeof resolveOperationHandler === "function"
|
|
6414
|
+
? resolveOperationHandler(operation)
|
|
6415
|
+
: handlers[operation];
|
|
5905
6416
|
|
|
5906
6417
|
if (!handler) {
|
|
5907
6418
|
figma.ui.postMessage({
|
package/plugin/ui.html
CHANGED
|
@@ -457,6 +457,18 @@
|
|
|
457
457
|
}
|
|
458
458
|
.t-tab:hover { color: var(--text-secondary); background: rgba(255,255,255,0.04); }
|
|
459
459
|
.t-tab.active { color: var(--text-primary); background: var(--surface-card); box-shadow: 0 1px 2px rgba(0,0,0,0.3); }
|
|
460
|
+
.t-tab.tab-err.active { color: var(--ghost-rose); }
|
|
461
|
+
.tab-badge-err {
|
|
462
|
+
font-size: 7.5px;
|
|
463
|
+
font-weight: 700;
|
|
464
|
+
color: var(--ghost-rose);
|
|
465
|
+
background: rgba(251, 113, 133, 0.18);
|
|
466
|
+
border-radius: var(--radius-full);
|
|
467
|
+
padding: 0 4px;
|
|
468
|
+
margin-left: 2px;
|
|
469
|
+
line-height: 1.3;
|
|
470
|
+
display: inline-block;
|
|
471
|
+
}
|
|
460
472
|
|
|
461
473
|
.terminal-meta { display: flex; align-items: center; gap: 6px; }
|
|
462
474
|
|
|
@@ -810,9 +822,16 @@
|
|
|
810
822
|
<button class="t-tab active" onclick="setLogFilter('all', this)">All</button>
|
|
811
823
|
<button class="t-tab" onclick="setLogFilter('writes', this)">Writes</button>
|
|
812
824
|
<button class="t-tab" onclick="setLogFilter('reads', this)">Reads</button>
|
|
825
|
+
<button class="t-tab tab-err" id="tab-errors" onclick="setLogFilter('errors', this)">Errors <span class="tab-badge-err" id="tab-err-badge" style="display:none">0</span></button>
|
|
813
826
|
</div>
|
|
814
827
|
<div class="terminal-meta">
|
|
815
828
|
<span class="event-badge" id="log-count">0 ev</span>
|
|
829
|
+
<button class="act-btn icon-only" onclick="copyLogs()" title="Copy console logs (respects active tab)" style="height:18px;width:18px;border-radius:3px;">
|
|
830
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
831
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
832
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
833
|
+
</svg>
|
|
834
|
+
</button>
|
|
816
835
|
<button class="act-btn icon-only" onclick="clearLog()" title="Clear stream console" style="height:18px;width:18px;border-radius:3px;">
|
|
817
836
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
818
837
|
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/>
|
|
@@ -933,15 +952,35 @@
|
|
|
933
952
|
if (el) el.classList.add("active");
|
|
934
953
|
|
|
935
954
|
var lines = logEl.querySelectorAll(".log-line");
|
|
955
|
+
var visibleCount = 0;
|
|
936
956
|
lines.forEach(function(line) {
|
|
957
|
+
var match = false;
|
|
937
958
|
if (filter === 'all') {
|
|
938
|
-
|
|
959
|
+
match = true;
|
|
939
960
|
} else if (filter === 'writes') {
|
|
940
|
-
|
|
961
|
+
match = line.dataset.type === 'write';
|
|
941
962
|
} else if (filter === 'reads') {
|
|
942
|
-
|
|
963
|
+
match = line.dataset.type === 'read';
|
|
964
|
+
} else if (filter === 'errors') {
|
|
965
|
+
match = line.dataset.cls === 'err';
|
|
943
966
|
}
|
|
967
|
+
line.style.display = match ? "flex" : "none";
|
|
968
|
+
if (match) visibleCount++;
|
|
944
969
|
});
|
|
970
|
+
|
|
971
|
+
if (logEmptyEl) {
|
|
972
|
+
if (visibleCount === 0 && totalLogEvents > 0) {
|
|
973
|
+
logEmptyEl.style.display = "flex";
|
|
974
|
+
var emptyText = logEmptyEl.querySelector(".terminal-empty-text");
|
|
975
|
+
if (emptyText) emptyText.textContent = filter === 'errors' ? "No errors logged" : "No matching logs";
|
|
976
|
+
} else if (totalLogEvents === 0) {
|
|
977
|
+
logEmptyEl.style.display = "flex";
|
|
978
|
+
var emptyText = logEmptyEl.querySelector(".terminal-empty-text");
|
|
979
|
+
if (emptyText) emptyText.textContent = "Listening on MCP bridge…";
|
|
980
|
+
} else {
|
|
981
|
+
logEmptyEl.style.display = "none";
|
|
982
|
+
}
|
|
983
|
+
}
|
|
945
984
|
}
|
|
946
985
|
|
|
947
986
|
async function fetchServerStats() {
|
|
@@ -1224,8 +1263,76 @@
|
|
|
1224
1263
|
}, 500);
|
|
1225
1264
|
}
|
|
1226
1265
|
|
|
1266
|
+
var totalErrors = 0;
|
|
1267
|
+
|
|
1268
|
+
function updateErrorBadge() {
|
|
1269
|
+
var badge = document.getElementById("tab-err-badge");
|
|
1270
|
+
if (!badge) return;
|
|
1271
|
+
if (totalErrors > 0) {
|
|
1272
|
+
badge.textContent = totalErrors;
|
|
1273
|
+
badge.style.display = "inline-block";
|
|
1274
|
+
} else {
|
|
1275
|
+
badge.style.display = "none";
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function copyTextToClipboard(text, successMsg) {
|
|
1280
|
+
if (!text) return;
|
|
1281
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1282
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
1283
|
+
showToast(successMsg || "Copied!", "✓");
|
|
1284
|
+
}).catch(function() {
|
|
1285
|
+
fallbackCopy(text, successMsg);
|
|
1286
|
+
});
|
|
1287
|
+
} else {
|
|
1288
|
+
fallbackCopy(text, successMsg);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function fallbackCopy(text, successMsg) {
|
|
1293
|
+
try {
|
|
1294
|
+
var ta = document.createElement("textarea");
|
|
1295
|
+
ta.value = text;
|
|
1296
|
+
ta.style.position = "fixed";
|
|
1297
|
+
ta.style.left = "-9999px";
|
|
1298
|
+
document.body.appendChild(ta);
|
|
1299
|
+
ta.select();
|
|
1300
|
+
document.execCommand("copy");
|
|
1301
|
+
document.body.removeChild(ta);
|
|
1302
|
+
showToast(successMsg || "Copied!", "✓");
|
|
1303
|
+
} catch(e) {
|
|
1304
|
+
showToast("Copy failed", "✗");
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function copyLogs() {
|
|
1309
|
+
var lines = logEl.querySelectorAll(".log-line");
|
|
1310
|
+
var visibleTexts = [];
|
|
1311
|
+
lines.forEach(function(line) {
|
|
1312
|
+
if (line.style.display !== "none") {
|
|
1313
|
+
var ts = line.querySelector(".log-ts");
|
|
1314
|
+
var msg = line.querySelector(".log-msg");
|
|
1315
|
+
var text = (ts ? ts.textContent + " " : "") + (msg ? msg.textContent : line.textContent);
|
|
1316
|
+
visibleTexts.push(text.trim());
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
if (visibleTexts.length === 0) {
|
|
1321
|
+
showToast(activeLogFilter === 'errors' ? "No errors to copy" : "No logs to copy", "ℹ");
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
var fullText = visibleTexts.join("\n");
|
|
1326
|
+
var label = activeLogFilter === 'errors' ? "Errors copied (" + visibleTexts.length + ")!" : "Logs copied (" + visibleTexts.length + ")!";
|
|
1327
|
+
copyTextToClipboard(fullText, label);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1227
1330
|
function log(msg, cls, opType) {
|
|
1228
1331
|
totalLogEvents++;
|
|
1332
|
+
if (cls === 'err') {
|
|
1333
|
+
totalErrors++;
|
|
1334
|
+
updateErrorBadge();
|
|
1335
|
+
}
|
|
1229
1336
|
if (logCountEl) logCountEl.textContent = totalLogEvents + " ev";
|
|
1230
1337
|
|
|
1231
1338
|
if (logEmptyEl) logEmptyEl.style.display = "none";
|
|
@@ -1235,6 +1342,9 @@
|
|
|
1235
1342
|
var line = document.createElement("div");
|
|
1236
1343
|
line.className = "log-line";
|
|
1237
1344
|
if (opType) line.dataset.type = opType;
|
|
1345
|
+
if (cls) line.dataset.cls = cls;
|
|
1346
|
+
line.title = "Click to copy";
|
|
1347
|
+
line.style.cursor = "pointer";
|
|
1238
1348
|
|
|
1239
1349
|
var ts = document.createElement("span");
|
|
1240
1350
|
ts.className = "log-ts";
|
|
@@ -1252,10 +1362,17 @@
|
|
|
1252
1362
|
line.appendChild(prompt);
|
|
1253
1363
|
line.appendChild(content);
|
|
1254
1364
|
|
|
1365
|
+
line.addEventListener("click", function() {
|
|
1366
|
+
var text = (ts ? ts.textContent + " " : "") + (content ? content.textContent : line.textContent);
|
|
1367
|
+
copyTextToClipboard(text.trim(), "Copied line!");
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1255
1370
|
if (activeLogFilter === 'writes' && opType !== 'write') {
|
|
1256
1371
|
line.style.display = "none";
|
|
1257
1372
|
} else if (activeLogFilter === 'reads' && opType !== 'read') {
|
|
1258
1373
|
line.style.display = "none";
|
|
1374
|
+
} else if (activeLogFilter === 'errors' && cls !== 'err') {
|
|
1375
|
+
line.style.display = "none";
|
|
1259
1376
|
}
|
|
1260
1377
|
|
|
1261
1378
|
logEl.appendChild(line);
|
|
@@ -1273,6 +1390,8 @@
|
|
|
1273
1390
|
function clearLog() {
|
|
1274
1391
|
logEl.innerHTML = "";
|
|
1275
1392
|
totalLogEvents = 0;
|
|
1393
|
+
totalErrors = 0;
|
|
1394
|
+
updateErrorBadge();
|
|
1276
1395
|
if (logCountEl) logCountEl.textContent = "0 ev";
|
|
1277
1396
|
var empty = document.createElement("div");
|
|
1278
1397
|
empty.id = "log-empty";
|