figma-rust-mcp 2.9.5 → 2.9.6
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 +216 -95
- package/plugin/ui.html +359 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-rust-mcp",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.6",
|
|
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-27T07:20:35.052Z
|
|
5
5
|
|
|
6
6
|
// ── plugin-src/utils.js ──
|
|
7
7
|
// ─── UTILS ────────────────────────────────────────────────────────────────────
|
|
@@ -328,6 +328,13 @@ function nodeToInfo(node) {
|
|
|
328
328
|
return info;
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
// Yield execution back to Figma main thread loop so UI never freezes/blocks
|
|
332
|
+
function yieldToUI(delayMs) {
|
|
333
|
+
return new Promise(function(resolve) {
|
|
334
|
+
setTimeout(resolve, delayMs !== undefined ? delayMs : 0);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
331
338
|
|
|
332
339
|
// ── plugin-src/svg-path-helpers.js ──
|
|
333
340
|
// ─── SVG PATH NORMALIZATION ───────────────────────────────────────────────────
|
|
@@ -1238,7 +1245,7 @@ const handlers = {};
|
|
|
1238
1245
|
|
|
1239
1246
|
handlers.status = async () => ({
|
|
1240
1247
|
connected: true,
|
|
1241
|
-
version: "2.
|
|
1248
|
+
version: "2.9.6",
|
|
1242
1249
|
fileName: figma.root.name,
|
|
1243
1250
|
currentPage: figma.currentPage.name,
|
|
1244
1251
|
pageCount: figma.root.children.length,
|
|
@@ -3035,60 +3042,98 @@ handlers.export_image = async function(params) {
|
|
|
3035
3042
|
};
|
|
3036
3043
|
};
|
|
3037
3044
|
|
|
3038
|
-
// index_scan — aggregate page nodes, styles, variables, and components in one call for pre-indexing
|
|
3045
|
+
// index_scan — aggregate page nodes, styles, variables, and components in one call for pre-indexing
|
|
3039
3046
|
handlers.index_scan = async function() {
|
|
3040
|
-
|
|
3047
|
+
if (figma.ui) {
|
|
3041
3048
|
try {
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
var CHUNK_SIZE = 10;
|
|
3046
|
-
|
|
3047
|
-
for (var i = 0; i < topFrames.length; i += CHUNK_SIZE) {
|
|
3048
|
-
var chunk = topFrames.slice(i, i + CHUNK_SIZE);
|
|
3049
|
-
var chunkData = chunk.map(function(n) {
|
|
3050
|
-
return Object.assign(nodeToInfo(n), { childCount: "children" in n ? n.children.length : 0 });
|
|
3051
|
-
});
|
|
3052
|
-
collected = collected.concat(chunkData);
|
|
3049
|
+
figma.ui.postMessage({ type: "index-progress", stage: "nodes", percent: 5, label: "Scanning canvas layers…" });
|
|
3050
|
+
} catch(e) {}
|
|
3051
|
+
}
|
|
3053
3052
|
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3053
|
+
var pageNodes = [];
|
|
3054
|
+
try {
|
|
3055
|
+
var page = figma.currentPage;
|
|
3056
|
+
var topFrames = page.children || [];
|
|
3057
|
+
var CHUNK_SIZE = 10;
|
|
3058
|
+
var totalFrames = topFrames.length;
|
|
3059
|
+
|
|
3060
|
+
for (var i = 0; i < topFrames.length; i += CHUNK_SIZE) {
|
|
3061
|
+
var chunk = topFrames.slice(i, i + CHUNK_SIZE);
|
|
3062
|
+
var chunkData = chunk.map(function(n) {
|
|
3063
|
+
return Object.assign(nodeToInfo(n), { childCount: "children" in n ? n.children.length : 0 });
|
|
3064
|
+
});
|
|
3065
|
+
pageNodes = pageNodes.concat(chunkData);
|
|
3066
|
+
|
|
3067
|
+
var nodePct = totalFrames > 0 ? Math.min(40, Math.round(5 + (pageNodes.length / totalFrames) * 35)) : 35;
|
|
3068
|
+
if (figma.ui) {
|
|
3069
|
+
try {
|
|
3070
|
+
figma.ui.postMessage({
|
|
3071
|
+
type: "index-progress",
|
|
3072
|
+
stage: "nodes",
|
|
3073
|
+
percent: nodePct,
|
|
3074
|
+
label: "Layers: " + pageNodes.length + (totalFrames ? "/" + totalFrames : "")
|
|
3075
|
+
});
|
|
3076
|
+
} catch(e) {}
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
// Stream chunk progressively if there are multiple chunks
|
|
3080
|
+
if (topFrames.length > CHUNK_SIZE && figma.ui) {
|
|
3081
|
+
try {
|
|
3082
|
+
figma.ui.postMessage({ type: "index-chunk", nodes: chunkData });
|
|
3083
|
+
} catch(e3) {}
|
|
3060
3084
|
}
|
|
3061
|
-
return collected;
|
|
3062
|
-
} catch (e) {}
|
|
3063
|
-
return [];
|
|
3064
|
-
})();
|
|
3065
3085
|
|
|
3066
|
-
|
|
3086
|
+
// Yield after every chunk so canvas/UI remains completely smooth
|
|
3087
|
+
if (typeof yieldToUI === "function") {
|
|
3088
|
+
await yieldToUI(0);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
} catch (e) {}
|
|
3092
|
+
|
|
3093
|
+
if (typeof yieldToUI === "function") await yieldToUI(0);
|
|
3094
|
+
|
|
3095
|
+
if (figma.ui) {
|
|
3067
3096
|
try {
|
|
3068
|
-
|
|
3069
|
-
} catch
|
|
3070
|
-
|
|
3071
|
-
|
|
3097
|
+
figma.ui.postMessage({ type: "index-progress", stage: "styles", percent: 45, label: "Indexing color & text styles…" });
|
|
3098
|
+
} catch(e) {}
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
var styles = null;
|
|
3102
|
+
try {
|
|
3103
|
+
if (handlers.get_styles) styles = await handlers.get_styles({});
|
|
3104
|
+
} catch (e) {}
|
|
3072
3105
|
|
|
3073
|
-
|
|
3106
|
+
if (typeof yieldToUI === "function") await yieldToUI(0);
|
|
3107
|
+
|
|
3108
|
+
if (figma.ui) {
|
|
3074
3109
|
try {
|
|
3075
|
-
|
|
3076
|
-
} catch
|
|
3077
|
-
|
|
3078
|
-
|
|
3110
|
+
figma.ui.postMessage({ type: "index-progress", stage: "variables", percent: 65, label: "Indexing variables & tokens…" });
|
|
3111
|
+
} catch(e) {}
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
var variables = null;
|
|
3115
|
+
try {
|
|
3116
|
+
if (handlers.get_variables) variables = await handlers.get_variables({});
|
|
3117
|
+
} catch (e) {}
|
|
3118
|
+
|
|
3119
|
+
if (typeof yieldToUI === "function") await yieldToUI(0);
|
|
3079
3120
|
|
|
3080
|
-
|
|
3121
|
+
if (figma.ui) {
|
|
3081
3122
|
try {
|
|
3082
|
-
|
|
3083
|
-
} catch
|
|
3084
|
-
|
|
3085
|
-
})();
|
|
3123
|
+
figma.ui.postMessage({ type: "index-progress", stage: "components", percent: 85, label: "Indexing components & sets…" });
|
|
3124
|
+
} catch(e) {}
|
|
3125
|
+
}
|
|
3086
3126
|
|
|
3087
|
-
var
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3127
|
+
var components = null;
|
|
3128
|
+
try {
|
|
3129
|
+
if (handlers.get_local_components) components = await handlers.get_local_components({});
|
|
3130
|
+
} catch (e) {}
|
|
3131
|
+
|
|
3132
|
+
if (figma.ui) {
|
|
3133
|
+
try {
|
|
3134
|
+
figma.ui.postMessage({ type: "index-progress", stage: "done", percent: 100, label: "Index ready" });
|
|
3135
|
+
} catch(e) {}
|
|
3136
|
+
}
|
|
3092
3137
|
|
|
3093
3138
|
var resolvedSid = figma.fileKey || (function() {
|
|
3094
3139
|
try { return figma.root.getPluginData("mcp_session_id") || "_default"; } catch(e) { return "_default"; }
|
|
@@ -3782,46 +3827,66 @@ handlers.get_styles = async function() {
|
|
|
3782
3827
|
|
|
3783
3828
|
// get_local_components — enhanced component listing with descriptions and properties
|
|
3784
3829
|
handlers.get_local_components = async function() {
|
|
3785
|
-
|
|
3830
|
+
if (typeof figma.loadAllPagesAsync === "function") {
|
|
3831
|
+
try { await figma.loadAllPagesAsync(); } catch (e) {}
|
|
3832
|
+
}
|
|
3786
3833
|
var comps = figma.root.findAllWithCriteria({ types: ["COMPONENT"] });
|
|
3834
|
+
if (typeof yieldToUI === "function") await yieldToUI(0);
|
|
3787
3835
|
var sets = figma.root.findAllWithCriteria({ types: ["COMPONENT_SET"] });
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3836
|
+
if (typeof yieldToUI === "function") await yieldToUI(0);
|
|
3837
|
+
|
|
3838
|
+
var componentList = [];
|
|
3839
|
+
var COMP_BATCH_SIZE = 50;
|
|
3840
|
+
for (var ci = 0; ci < comps.length; ci++) {
|
|
3841
|
+
var c = comps[ci];
|
|
3842
|
+
var info = {
|
|
3843
|
+
id: c.id, name: c.name, key: c.key || null,
|
|
3844
|
+
description: c.description || "",
|
|
3845
|
+
setName: (c.parent && c.parent.type === "COMPONENT_SET") ? c.parent.name : null,
|
|
3846
|
+
width: Math.round(c.width), height: Math.round(c.height),
|
|
3847
|
+
page: c.parent ? (function findPage(n) {
|
|
3848
|
+
while (n && n.type !== "PAGE") n = n.parent;
|
|
3849
|
+
return n ? n.name : null;
|
|
3850
|
+
})(c) : null,
|
|
3851
|
+
};
|
|
3852
|
+
// Component properties (variant props)
|
|
3853
|
+
try {
|
|
3854
|
+
if (c.componentPropertyDefinitions) {
|
|
3855
|
+
var defs = c.componentPropertyDefinitions;
|
|
3856
|
+
var props = {};
|
|
3857
|
+
for (var key in defs) {
|
|
3858
|
+
if (Object.prototype.hasOwnProperty.call(defs, key)) {
|
|
3859
|
+
props[key] = { type: defs[key].type, defaultValue: defs[key].defaultValue };
|
|
3860
|
+
if (defs[key].variantOptions) props[key].options = defs[key].variantOptions;
|
|
3811
3861
|
}
|
|
3812
|
-
info.properties = props;
|
|
3813
3862
|
}
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
})
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3863
|
+
info.properties = props;
|
|
3864
|
+
}
|
|
3865
|
+
} catch(e) { /* skip properties */ }
|
|
3866
|
+
componentList.push(info);
|
|
3867
|
+
|
|
3868
|
+
if (ci > 0 && ci % COMP_BATCH_SIZE === 0 && typeof yieldToUI === "function") {
|
|
3869
|
+
await yieldToUI(0);
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
var componentSets = [];
|
|
3874
|
+
for (var si = 0; si < sets.length; si++) {
|
|
3875
|
+
var s = sets[si];
|
|
3876
|
+
componentSets.push({
|
|
3877
|
+
id: s.id, name: s.name, key: s.key || null,
|
|
3878
|
+
description: s.description || "",
|
|
3879
|
+
variantCount: s.children ? s.children.length : 0,
|
|
3880
|
+
});
|
|
3881
|
+
if (si > 0 && si % COMP_BATCH_SIZE === 0 && typeof yieldToUI === "function") {
|
|
3882
|
+
await yieldToUI(0);
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
return {
|
|
3887
|
+
components: componentList,
|
|
3888
|
+
componentSets: componentSets,
|
|
3889
|
+
total: componentList.length + componentSets.length,
|
|
3825
3890
|
};
|
|
3826
3891
|
};
|
|
3827
3892
|
|
|
@@ -3904,12 +3969,17 @@ handlers.get_variables = async function() {
|
|
|
3904
3969
|
values: values,
|
|
3905
3970
|
description: v.description || "",
|
|
3906
3971
|
});
|
|
3972
|
+
|
|
3973
|
+
if (vi > 0 && vi % 50 === 0 && typeof yieldToUI === "function") {
|
|
3974
|
+
await yieldToUI(0);
|
|
3975
|
+
}
|
|
3907
3976
|
}
|
|
3908
3977
|
collections.push({
|
|
3909
3978
|
id: col.id, name: col.name,
|
|
3910
3979
|
modes: col.modes.map(function(m) { return { id: m.modeId, name: m.name }; }),
|
|
3911
3980
|
variables: variables,
|
|
3912
3981
|
});
|
|
3982
|
+
if (typeof yieldToUI === "function") await yieldToUI(0);
|
|
3913
3983
|
}
|
|
3914
3984
|
} catch(e) {
|
|
3915
3985
|
return { error: "Variables API not available: " + e.message, collections: [] };
|
|
@@ -5310,9 +5380,19 @@ figma.clientStorage.getAsync("mcp_window_size").then(function(saved) {
|
|
|
5310
5380
|
// In Figma, figma.root.id is always "0:0" across all files. To support running
|
|
5311
5381
|
// multiple plugins concurrently across multiple tabs/files, generate and persist
|
|
5312
5382
|
// a unique document session ID in document pluginData.
|
|
5383
|
+
function getSavedFileKey() {
|
|
5384
|
+
if (figma.fileKey) return figma.fileKey;
|
|
5385
|
+
try {
|
|
5386
|
+
var saved = figma.root.getPluginData("figma_file_key");
|
|
5387
|
+
if (saved && saved.length > 0) return saved;
|
|
5388
|
+
} catch(e) {}
|
|
5389
|
+
return null;
|
|
5390
|
+
}
|
|
5391
|
+
|
|
5313
5392
|
function getOrCreateSessionId() {
|
|
5314
|
-
|
|
5315
|
-
|
|
5393
|
+
var fk = getSavedFileKey();
|
|
5394
|
+
if (fk) {
|
|
5395
|
+
return fk;
|
|
5316
5396
|
}
|
|
5317
5397
|
try {
|
|
5318
5398
|
var stored = figma.root.getPluginData("mcp_session_id");
|
|
@@ -5332,13 +5412,15 @@ function getOrCreateSessionId() {
|
|
|
5332
5412
|
|
|
5333
5413
|
var currentSessionId = getOrCreateSessionId();
|
|
5334
5414
|
var currentFileName = figma.root ? figma.root.name : "Untitled";
|
|
5415
|
+
var currentFileKey = getSavedFileKey();
|
|
5335
5416
|
|
|
5336
5417
|
// Broadcast active file / session metadata on startup
|
|
5337
5418
|
try {
|
|
5338
5419
|
figma.ui.postMessage({
|
|
5339
5420
|
type: "session-info",
|
|
5340
5421
|
sessionId: currentSessionId,
|
|
5341
|
-
fileName: currentFileName
|
|
5422
|
+
fileName: currentFileName,
|
|
5423
|
+
fileKey: currentFileKey
|
|
5342
5424
|
});
|
|
5343
5425
|
} catch (e) {}
|
|
5344
5426
|
|
|
@@ -5350,8 +5432,15 @@ figma.on("selectionchange", function() {
|
|
|
5350
5432
|
var fullNode = null;
|
|
5351
5433
|
for (var i = 0; i < Math.min(sel.length, 5); i++) {
|
|
5352
5434
|
var n = sel[i];
|
|
5435
|
+
// If node is an internal instance sub-layer (starts with I...), find its main component instance or top frame
|
|
5436
|
+
var mainId = n.id;
|
|
5437
|
+
if (mainId.startsWith("I") && mainId.indexOf(";") !== -1) {
|
|
5438
|
+
var parts = mainId.split(";");
|
|
5439
|
+
mainId = parts[parts.length - 1]; // Use real canonical node id
|
|
5440
|
+
}
|
|
5353
5441
|
summary.push({
|
|
5354
5442
|
id: n.id,
|
|
5443
|
+
mainId: mainId,
|
|
5355
5444
|
name: n.name,
|
|
5356
5445
|
type: n.type,
|
|
5357
5446
|
width: typeof n.width === "number" ? Math.round(n.width) : undefined,
|
|
@@ -5433,20 +5522,23 @@ function onDocChange(event) {
|
|
|
5433
5522
|
} catch (e) {}
|
|
5434
5523
|
}
|
|
5435
5524
|
|
|
5436
|
-
// In dynamic-page mode, Figma requires figma.loadAllPagesAsync() before registering documentchange
|
|
5437
|
-
|
|
5438
|
-
|
|
5525
|
+
// In dynamic-page mode, Figma requires figma.loadAllPagesAsync() before registering documentchange.
|
|
5526
|
+
// Delay loading all pages slightly so plugin UI opens and paints immediately without UI freeze.
|
|
5527
|
+
setTimeout(function() {
|
|
5528
|
+
if (typeof figma.loadAllPagesAsync === "function") {
|
|
5529
|
+
figma.loadAllPagesAsync().then(function() {
|
|
5530
|
+
try {
|
|
5531
|
+
figma.on("documentchange", onDocChange);
|
|
5532
|
+
} catch (e) {}
|
|
5533
|
+
}).catch(function() {});
|
|
5534
|
+
} else {
|
|
5439
5535
|
try {
|
|
5440
5536
|
figma.on("documentchange", onDocChange);
|
|
5441
5537
|
} catch (e) {}
|
|
5442
|
-
}
|
|
5443
|
-
}
|
|
5444
|
-
try {
|
|
5445
|
-
figma.on("documentchange", onDocChange);
|
|
5446
|
-
} catch (e) {}
|
|
5447
|
-
}
|
|
5538
|
+
}
|
|
5539
|
+
}, 800);
|
|
5448
5540
|
|
|
5449
|
-
// Background initial scan after 1.
|
|
5541
|
+
// Background initial scan after 1.8s startup delay (ensures UI is fully rendered and WebSocket connected)
|
|
5450
5542
|
setTimeout(async function() {
|
|
5451
5543
|
try {
|
|
5452
5544
|
if (handlers.index_scan) {
|
|
@@ -5461,7 +5553,7 @@ setTimeout(async function() {
|
|
|
5461
5553
|
});
|
|
5462
5554
|
}
|
|
5463
5555
|
} catch (e) {}
|
|
5464
|
-
},
|
|
5556
|
+
}, 1800);
|
|
5465
5557
|
|
|
5466
5558
|
// ─── DISPATCHER ───────────────────────────────────────────────────────────────
|
|
5467
5559
|
|
|
@@ -5508,6 +5600,35 @@ figma.ui.onmessage = async (request) => {
|
|
|
5508
5600
|
return;
|
|
5509
5601
|
}
|
|
5510
5602
|
|
|
5603
|
+
// Set and persist Figma File Key
|
|
5604
|
+
if (request.type === "set-file-key") {
|
|
5605
|
+
try {
|
|
5606
|
+
var newFileKey = (request.fileKey || "").trim();
|
|
5607
|
+
if (newFileKey) {
|
|
5608
|
+
figma.root.setPluginData("figma_file_key", newFileKey);
|
|
5609
|
+
currentFileKey = newFileKey;
|
|
5610
|
+
figma.ui.postMessage({
|
|
5611
|
+
type: "session-info",
|
|
5612
|
+
sessionId: currentSessionId,
|
|
5613
|
+
fileName: currentFileName,
|
|
5614
|
+
fileKey: newFileKey
|
|
5615
|
+
});
|
|
5616
|
+
figma.notify("Figma File Key saved!", { timeout: 1500 });
|
|
5617
|
+
}
|
|
5618
|
+
} catch(e) {}
|
|
5619
|
+
return;
|
|
5620
|
+
}
|
|
5621
|
+
|
|
5622
|
+
// Toast notification from UI
|
|
5623
|
+
if (request.type === "notify") {
|
|
5624
|
+
try {
|
|
5625
|
+
if (request.message) {
|
|
5626
|
+
figma.notify(request.message, { timeout: request.timeout || 1500, error: request.error || false });
|
|
5627
|
+
}
|
|
5628
|
+
} catch(e) {}
|
|
5629
|
+
return;
|
|
5630
|
+
}
|
|
5631
|
+
|
|
5511
5632
|
// Zoom to selection when clicked on selection bar
|
|
5512
5633
|
if (request.type === "zoom-to-selection") {
|
|
5513
5634
|
try {
|
package/plugin/ui.html
CHANGED
|
@@ -98,8 +98,68 @@
|
|
|
98
98
|
padding: 6px 8px;
|
|
99
99
|
flex-shrink: 0;
|
|
100
100
|
box-shadow: 0 1px 2px rgba(0,0,0,0.4);
|
|
101
|
+
position: relative;
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
/* ── INDEXING PROGRESS BAR (Embedded in Header) ── */
|
|
105
|
+
.index-progress-wrap {
|
|
106
|
+
display: none;
|
|
107
|
+
flex-direction: column;
|
|
108
|
+
gap: 3px;
|
|
109
|
+
padding: 4px 8px 6px;
|
|
110
|
+
background: rgba(147, 129, 255, 0.08);
|
|
111
|
+
border: 1px solid rgba(147, 129, 255, 0.28);
|
|
112
|
+
border-radius: var(--radius-sm);
|
|
113
|
+
margin-top: 1px;
|
|
114
|
+
animation: fadeIn 0.2s ease-out;
|
|
115
|
+
}
|
|
116
|
+
.index-progress-wrap.active {
|
|
117
|
+
display: flex;
|
|
118
|
+
}
|
|
119
|
+
.index-progress-header {
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
justify-content: space-between;
|
|
123
|
+
font-size: 9px;
|
|
124
|
+
font-weight: 600;
|
|
125
|
+
color: var(--ghost-lavender);
|
|
126
|
+
font-family: var(--font-mono);
|
|
127
|
+
}
|
|
128
|
+
.index-progress-label {
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: 4px;
|
|
132
|
+
white-space: nowrap;
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
text-overflow: ellipsis;
|
|
135
|
+
max-width: 220px;
|
|
136
|
+
}
|
|
137
|
+
.index-progress-label svg {
|
|
138
|
+
animation: spin 1s linear infinite;
|
|
139
|
+
flex-shrink: 0;
|
|
140
|
+
}
|
|
141
|
+
.index-progress-pct {
|
|
142
|
+
color: var(--text-primary);
|
|
143
|
+
flex-shrink: 0;
|
|
144
|
+
}
|
|
145
|
+
.index-progress-track {
|
|
146
|
+
width: 100%;
|
|
147
|
+
height: 3px;
|
|
148
|
+
background: rgba(255, 255, 255, 0.08);
|
|
149
|
+
border-radius: 9999px;
|
|
150
|
+
overflow: hidden;
|
|
151
|
+
}
|
|
152
|
+
.index-progress-fill {
|
|
153
|
+
height: 100%;
|
|
154
|
+
width: 0%;
|
|
155
|
+
background: linear-gradient(90deg, #7c3aed, #9381ff, #34d399);
|
|
156
|
+
border-radius: 9999px;
|
|
157
|
+
transition: width 0.2s ease-out;
|
|
158
|
+
box-shadow: 0 0 8px rgba(147, 129, 255, 0.6);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: translateY(0); } }
|
|
162
|
+
|
|
103
163
|
.brand { display: flex; align-items: center; gap: 7px; min-width: 0; }
|
|
104
164
|
|
|
105
165
|
.brand-logo {
|
|
@@ -517,19 +577,82 @@
|
|
|
517
577
|
pointer-events: auto;
|
|
518
578
|
}
|
|
519
579
|
|
|
520
|
-
/*
|
|
521
|
-
.
|
|
522
|
-
position: fixed;
|
|
523
|
-
|
|
524
|
-
|
|
580
|
+
/* ── CUSTOM IN-PLUGIN MODAL DIALOG (No window.prompt dependency) ── */
|
|
581
|
+
.modal-overlay {
|
|
582
|
+
position: fixed;
|
|
583
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
584
|
+
background: rgba(8, 9, 13, 0.75);
|
|
585
|
+
backdrop-filter: blur(5px);
|
|
586
|
+
display: flex;
|
|
587
|
+
align-items: center;
|
|
588
|
+
justify-content: center;
|
|
589
|
+
padding: 12px;
|
|
590
|
+
z-index: 999;
|
|
591
|
+
opacity: 0;
|
|
592
|
+
pointer-events: none;
|
|
593
|
+
transition: opacity var(--transition);
|
|
594
|
+
}
|
|
595
|
+
.modal-overlay.show {
|
|
596
|
+
opacity: 1;
|
|
597
|
+
pointer-events: auto;
|
|
598
|
+
}
|
|
599
|
+
.modal-card {
|
|
600
|
+
background: var(--surface-card);
|
|
601
|
+
border: 1px solid var(--border-light);
|
|
602
|
+
border-radius: var(--radius-md);
|
|
603
|
+
box-shadow: 0 16px 36px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.08) inset;
|
|
604
|
+
width: 100%;
|
|
605
|
+
max-width: 290px;
|
|
606
|
+
padding: 12px;
|
|
607
|
+
display: flex;
|
|
608
|
+
flex-direction: column;
|
|
609
|
+
gap: 10px;
|
|
610
|
+
transform: scale(0.95);
|
|
611
|
+
transition: transform var(--transition);
|
|
612
|
+
}
|
|
613
|
+
.modal-overlay.show .modal-card {
|
|
614
|
+
transform: scale(1);
|
|
615
|
+
}
|
|
616
|
+
.modal-title-row {
|
|
617
|
+
display: flex;
|
|
618
|
+
align-items: center;
|
|
619
|
+
justify-content: space-between;
|
|
620
|
+
}
|
|
621
|
+
.modal-title {
|
|
622
|
+
font-size: 11px;
|
|
623
|
+
font-weight: 700;
|
|
624
|
+
color: var(--text-primary);
|
|
625
|
+
display: flex;
|
|
626
|
+
align-items: center;
|
|
627
|
+
gap: 5px;
|
|
628
|
+
}
|
|
629
|
+
.modal-desc {
|
|
630
|
+
font-size: 9.5px;
|
|
525
631
|
color: var(--text-secondary);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
632
|
+
line-height: 1.35;
|
|
633
|
+
}
|
|
634
|
+
.modal-input {
|
|
635
|
+
width: 100%;
|
|
636
|
+
background: var(--surface-base);
|
|
637
|
+
border: 1px solid var(--border-light);
|
|
638
|
+
border-radius: var(--radius-xs);
|
|
639
|
+
color: var(--text-primary);
|
|
640
|
+
font-family: var(--font-mono);
|
|
641
|
+
font-size: 9.5px;
|
|
642
|
+
padding: 6px 8px;
|
|
643
|
+
outline: none;
|
|
644
|
+
transition: border-color var(--transition);
|
|
645
|
+
}
|
|
646
|
+
.modal-input:focus {
|
|
647
|
+
border-color: var(--ghost-lavender);
|
|
648
|
+
box-shadow: 0 0 0 2px var(--ghost-glow);
|
|
649
|
+
}
|
|
650
|
+
.modal-actions {
|
|
651
|
+
display: flex;
|
|
652
|
+
justify-content: flex-end;
|
|
653
|
+
gap: 6px;
|
|
654
|
+
margin-top: 2px;
|
|
531
655
|
}
|
|
532
|
-
.resize-grip:hover { opacity: 1; color: var(--ghost-mint); }
|
|
533
656
|
</style>
|
|
534
657
|
</head>
|
|
535
658
|
<body>
|
|
@@ -548,7 +671,7 @@
|
|
|
548
671
|
<div class="brand-info">
|
|
549
672
|
<div class="brand-title-row">
|
|
550
673
|
<span class="brand-name">Figma MCP</span>
|
|
551
|
-
<span class="brand-ver">v2.
|
|
674
|
+
<span class="brand-ver">v2.9.6</span>
|
|
552
675
|
</div>
|
|
553
676
|
<span class="brand-file" id="doc-display" title="Active Document">Ready</span>
|
|
554
677
|
</div>
|
|
@@ -571,6 +694,14 @@
|
|
|
571
694
|
</svg>
|
|
572
695
|
</button>
|
|
573
696
|
|
|
697
|
+
<!-- Copy File / Session Link -->
|
|
698
|
+
<button class="header-icon-btn" id="file-link-btn" onclick="copyFileLink(event)" title="🔗 Set / Copy Figma File Link">
|
|
699
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
700
|
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
|
701
|
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
|
702
|
+
</svg>
|
|
703
|
+
</button>
|
|
704
|
+
|
|
574
705
|
<!-- Connection Status Pill -->
|
|
575
706
|
<div class="status-pill" id="status-pill" onclick="handleStatusClick()" title="Connection Status (Click for details)">
|
|
576
707
|
<div class="dot yellow" id="dot"></div>
|
|
@@ -579,6 +710,22 @@
|
|
|
579
710
|
</div>
|
|
580
711
|
</header>
|
|
581
712
|
|
|
713
|
+
<!-- ── INDEXING PROGRESS HUD ── -->
|
|
714
|
+
<div class="index-progress-wrap" id="index-progress-wrap">
|
|
715
|
+
<div class="index-progress-header">
|
|
716
|
+
<span class="index-progress-label" id="index-progress-label">
|
|
717
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
718
|
+
<polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
|
719
|
+
</svg>
|
|
720
|
+
<span id="index-progress-text">Indexing canvas layers…</span>
|
|
721
|
+
</span>
|
|
722
|
+
<span class="index-progress-pct" id="index-progress-pct">0%</span>
|
|
723
|
+
</div>
|
|
724
|
+
<div class="index-progress-track">
|
|
725
|
+
<div class="index-progress-fill" id="index-progress-fill"></div>
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
|
|
582
729
|
<!-- ── SELECTION INSPECTOR HERO CARD ── -->
|
|
583
730
|
<div class="selection-card" id="selection-card">
|
|
584
731
|
<div class="selection-main" onclick="focusSelection()" title="Click to center view on selection">
|
|
@@ -602,6 +749,14 @@
|
|
|
602
749
|
</svg>
|
|
603
750
|
</button>
|
|
604
751
|
|
|
752
|
+
<!-- Copy Link to Selection Frame button -->
|
|
753
|
+
<button class="act-btn icon-only" id="link-btn" onclick="copySelectionLink(event)" title="🔗 Copy link to selection frame">
|
|
754
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
755
|
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
|
756
|
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
|
757
|
+
</svg>
|
|
758
|
+
</button>
|
|
759
|
+
|
|
605
760
|
<!-- Focus / Center on Canvas button -->
|
|
606
761
|
<button class="act-btn icon-only" onclick="focusSelection()" title="🎯 Center view on canvas">
|
|
607
762
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -613,7 +768,15 @@
|
|
|
613
768
|
</svg>
|
|
614
769
|
</button>
|
|
615
770
|
|
|
616
|
-
<!-- Copy Node ID
|
|
771
|
+
<!-- Copy Node ID button -->
|
|
772
|
+
<button class="act-btn icon-only" id="copy-id-btn" onclick="copySelectionId(event)" title="📋 Copy Figma Node ID">
|
|
773
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
774
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
|
775
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
776
|
+
</svg>
|
|
777
|
+
</button>
|
|
778
|
+
|
|
779
|
+
<!-- Node Type Badge button (also clickable to copy ID) -->
|
|
617
780
|
<button class="act-btn badge-btn" id="selection-badge" onclick="copySelectionId(event)" title="📋 Click to copy Figma Node ID">
|
|
618
781
|
FRAME
|
|
619
782
|
</button>
|
|
@@ -667,6 +830,29 @@
|
|
|
667
830
|
|
|
668
831
|
</div>
|
|
669
832
|
|
|
833
|
+
<!-- File Key Input Modal Dialog -->
|
|
834
|
+
<div class="modal-overlay" id="key-modal-overlay">
|
|
835
|
+
<div class="modal-card">
|
|
836
|
+
<div class="modal-title-row">
|
|
837
|
+
<div class="modal-title">
|
|
838
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--ghost-lavender)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
839
|
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
|
840
|
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
|
841
|
+
</svg>
|
|
842
|
+
Set Figma File Link
|
|
843
|
+
</div>
|
|
844
|
+
</div>
|
|
845
|
+
<div class="modal-desc">
|
|
846
|
+
Paste full Figma file URL or File Key to generate real browser links for your layers:
|
|
847
|
+
</div>
|
|
848
|
+
<input type="text" class="modal-input" id="file-key-input" placeholder="https://www.figma.com/design/..." spellcheck="false" autocomplete="off" />
|
|
849
|
+
<div class="modal-actions">
|
|
850
|
+
<button class="btn" onclick="closeKeyModal()">Cancel</button>
|
|
851
|
+
<button class="btn btn-primary" onclick="saveFileKeyFromModal()">Save & Copy</button>
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
</div>
|
|
855
|
+
|
|
670
856
|
<!-- Floating Toast Component -->
|
|
671
857
|
<div class="toast" id="toast">
|
|
672
858
|
<span id="toast-icon">✓</span>
|
|
@@ -691,6 +877,7 @@
|
|
|
691
877
|
var lastSuccessfulPollAt = Date.now();
|
|
692
878
|
var sessionId = null;
|
|
693
879
|
var fileName = null;
|
|
880
|
+
var fileKey = null;
|
|
694
881
|
|
|
695
882
|
var writeCount = 0, readCount = 0, errCount = 0;
|
|
696
883
|
var totalLogEvents = 0;
|
|
@@ -708,6 +895,10 @@
|
|
|
708
895
|
var ramEl = document.getElementById("ram-display");
|
|
709
896
|
var reindexBtn = document.getElementById("reindex-btn");
|
|
710
897
|
var reindexBtnLabel = document.getElementById("reindex-btn-label");
|
|
898
|
+
var indexProgressWrap = document.getElementById("index-progress-wrap");
|
|
899
|
+
var indexProgressText = document.getElementById("index-progress-text");
|
|
900
|
+
var indexProgressPct = document.getElementById("index-progress-pct");
|
|
901
|
+
var indexProgressFill = document.getElementById("index-progress-fill");
|
|
711
902
|
|
|
712
903
|
var selCardEl = document.getElementById("selection-card");
|
|
713
904
|
var selIconEl = document.getElementById("selection-icon");
|
|
@@ -722,6 +913,7 @@
|
|
|
722
913
|
|
|
723
914
|
var currentSelectionCount = 0;
|
|
724
915
|
var currentSelectionId = null;
|
|
916
|
+
var currentSelectionMainId = null;
|
|
725
917
|
var currentSelectionName = "";
|
|
726
918
|
|
|
727
919
|
function showToast(msg, icon) {
|
|
@@ -791,15 +983,149 @@
|
|
|
791
983
|
parent.postMessage({ pluginMessage: { type: "export-selection-image", scale: 2 } }, "*");
|
|
792
984
|
}
|
|
793
985
|
|
|
986
|
+
function copyTextFallback(text) {
|
|
987
|
+
try {
|
|
988
|
+
var textarea = document.createElement("textarea");
|
|
989
|
+
textarea.value = text;
|
|
990
|
+
textarea.style.position = "fixed";
|
|
991
|
+
textarea.style.left = "-9999px";
|
|
992
|
+
textarea.style.top = "-9999px";
|
|
993
|
+
textarea.style.opacity = "0";
|
|
994
|
+
document.body.appendChild(textarea);
|
|
995
|
+
textarea.focus();
|
|
996
|
+
textarea.select();
|
|
997
|
+
var successful = document.execCommand("copy");
|
|
998
|
+
document.body.removeChild(textarea);
|
|
999
|
+
return successful;
|
|
1000
|
+
} catch (err) {
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function openKeyModal(onSuccess) {
|
|
1006
|
+
var modal = document.getElementById("key-modal-overlay");
|
|
1007
|
+
var input = document.getElementById("file-key-input");
|
|
1008
|
+
if (modal && input) {
|
|
1009
|
+
input.value = fileKey ? ("https://www.figma.com/design/" + fileKey) : "";
|
|
1010
|
+
modal.classList.add("show");
|
|
1011
|
+
window.__onKeySaved = onSuccess;
|
|
1012
|
+
setTimeout(function() { input.focus(); input.select(); }, 50);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function closeKeyModal() {
|
|
1017
|
+
var modal = document.getElementById("key-modal-overlay");
|
|
1018
|
+
if (modal) modal.classList.remove("show");
|
|
1019
|
+
window.__onKeySaved = null;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function saveFileKeyFromModal() {
|
|
1023
|
+
var input = document.getElementById("file-key-input");
|
|
1024
|
+
var val = input ? input.value.trim() : "";
|
|
1025
|
+
if (val) {
|
|
1026
|
+
var match = val.match(/(?:file|design)\/([a-zA-Z0-9]+)/);
|
|
1027
|
+
var parsedKey = match ? match[1] : val;
|
|
1028
|
+
fileKey = parsedKey;
|
|
1029
|
+
parent.postMessage({ pluginMessage: { type: "set-file-key", fileKey: parsedKey } }, "*");
|
|
1030
|
+
showToast("File Key saved!", "🔗");
|
|
1031
|
+
}
|
|
1032
|
+
var cb = window.__onKeySaved;
|
|
1033
|
+
closeKeyModal();
|
|
1034
|
+
if (typeof cb === "function") {
|
|
1035
|
+
cb(fileKey);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function copyFileLink(event) {
|
|
1040
|
+
if (event) {
|
|
1041
|
+
event.stopPropagation();
|
|
1042
|
+
event.preventDefault();
|
|
1043
|
+
}
|
|
1044
|
+
openKeyModal(function(savedKey) {
|
|
1045
|
+
if (savedKey) {
|
|
1046
|
+
showToast("File link updated", "🔗");
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function copySelectionLink(event) {
|
|
1052
|
+
if (event) {
|
|
1053
|
+
event.stopPropagation();
|
|
1054
|
+
event.preventDefault();
|
|
1055
|
+
}
|
|
1056
|
+
if (!currentSelectionId) {
|
|
1057
|
+
showToast("No selection active", "⚠️");
|
|
1058
|
+
try { parent.postMessage({ pluginMessage: { type: "notify", message: "⚠️ No frame selected to copy link", error: true } }, "*"); } catch(e) {}
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (!fileKey) {
|
|
1063
|
+
openKeyModal(function(savedKey) {
|
|
1064
|
+
executeCopyLink(savedKey);
|
|
1065
|
+
});
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
executeCopyLink(fileKey);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function executeCopyLink(effectiveKey) {
|
|
1073
|
+
if (!currentSelectionId) return;
|
|
1074
|
+
|
|
1075
|
+
var targetId = currentSelectionMainId || currentSelectionId;
|
|
1076
|
+
var figmaNodeId = targetId.replace(/:/g, "-");
|
|
1077
|
+
|
|
1078
|
+
// Figma web link uses a sanitized slug of the file name (or untitled)
|
|
1079
|
+
var cleanSlug = (fileName || "file")
|
|
1080
|
+
.replace(/[\[\]\(\)\{\}]/g, "")
|
|
1081
|
+
.trim()
|
|
1082
|
+
.replace(/\s+/g, "-");
|
|
1083
|
+
var encodedSlug = encodeURIComponent(cleanSlug);
|
|
1084
|
+
|
|
1085
|
+
var link = "";
|
|
1086
|
+
if (effectiveKey) {
|
|
1087
|
+
link = "https://www.figma.com/design/" + effectiveKey + "/" + encodedSlug + "?node-id=" + figmaNodeId;
|
|
1088
|
+
} else {
|
|
1089
|
+
link = "https://www.figma.com/design/draft/" + encodedSlug + "?node-id=" + figmaNodeId;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
var copied = copyTextFallback(link);
|
|
1093
|
+
if (!copied && navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
|
1094
|
+
navigator.clipboard.writeText(link).catch(function() {});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
showToast("Link: " + figmaNodeId, "🔗");
|
|
1098
|
+
try {
|
|
1099
|
+
parent.postMessage({
|
|
1100
|
+
pluginMessage: {
|
|
1101
|
+
type: "notify",
|
|
1102
|
+
message: "🔗 Link copied to clipboard! (" + figmaNodeId + ")"
|
|
1103
|
+
}
|
|
1104
|
+
}, "*");
|
|
1105
|
+
} catch(e) {}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
794
1108
|
function copySelectionId(event) {
|
|
795
|
-
if (event)
|
|
1109
|
+
if (event) {
|
|
1110
|
+
event.stopPropagation();
|
|
1111
|
+
event.preventDefault();
|
|
1112
|
+
}
|
|
796
1113
|
if (!currentSelectionId) return;
|
|
797
|
-
|
|
1114
|
+
var copied = copyTextFallback(currentSelectionId);
|
|
1115
|
+
if (!copied && navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
|
798
1116
|
navigator.clipboard.writeText(currentSelectionId).then(function() {
|
|
799
1117
|
showToast("Node ID: " + currentSelectionId + " copied", "📋");
|
|
1118
|
+
try {
|
|
1119
|
+
parent.postMessage({ pluginMessage: { type: "notify", message: "📋 Node ID: " + currentSelectionId + " copied" } }, "*");
|
|
1120
|
+
} catch(e) {}
|
|
800
1121
|
}).catch(function() {
|
|
801
1122
|
prompt("Copy Node ID:", currentSelectionId);
|
|
802
1123
|
});
|
|
1124
|
+
} else if (copied) {
|
|
1125
|
+
showToast("Node ID: " + currentSelectionId + " copied", "📋");
|
|
1126
|
+
try {
|
|
1127
|
+
parent.postMessage({ pluginMessage: { type: "notify", message: "📋 Node ID: " + currentSelectionId + " copied" } }, "*");
|
|
1128
|
+
} catch(e) {}
|
|
803
1129
|
} else {
|
|
804
1130
|
prompt("Copy Node ID:", currentSelectionId);
|
|
805
1131
|
}
|
|
@@ -1259,6 +1585,7 @@
|
|
|
1259
1585
|
var list = msg.selection || [];
|
|
1260
1586
|
currentSelectionCount = count;
|
|
1261
1587
|
currentSelectionId = (count === 1 && list[0]) ? list[0].id : null;
|
|
1588
|
+
currentSelectionMainId = (count === 1 && list[0]) ? (list[0].mainId || list[0].id) : null;
|
|
1262
1589
|
currentSelectionName = (count === 1 && list[0]) ? (list[0].name || "Layer") : "";
|
|
1263
1590
|
|
|
1264
1591
|
if (count === 0) {
|
|
@@ -1302,6 +1629,7 @@
|
|
|
1302
1629
|
var prevSid = sessionId;
|
|
1303
1630
|
sessionId = msg.sessionId || null;
|
|
1304
1631
|
fileName = msg.fileName || null;
|
|
1632
|
+
fileKey = msg.fileKey || null;
|
|
1305
1633
|
if (fileName && docDispEl) {
|
|
1306
1634
|
docDispEl.textContent = fileName;
|
|
1307
1635
|
docDispEl.title = fileName + " (" + (sessionId || "active session") + ")";
|
|
@@ -1373,6 +1701,15 @@
|
|
|
1373
1701
|
return;
|
|
1374
1702
|
}
|
|
1375
1703
|
|
|
1704
|
+
if (msg.type === "index-progress") {
|
|
1705
|
+
if (reindexBtn) reindexBtn.classList.add("indexing");
|
|
1706
|
+
if (indexProgressWrap) indexProgressWrap.classList.add("active");
|
|
1707
|
+
if (indexProgressText && msg.label) indexProgressText.textContent = msg.label;
|
|
1708
|
+
if (indexProgressPct && msg.percent !== undefined) indexProgressPct.textContent = msg.percent + "%";
|
|
1709
|
+
if (indexProgressFill && msg.percent !== undefined) indexProgressFill.style.width = Math.min(100, Math.max(0, msg.percent)) + "%";
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1376
1713
|
if (msg.type === "index-chunk") {
|
|
1377
1714
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1378
1715
|
try { ws.send(JSON.stringify({ type: "index-chunk", nodes: msg.nodes })); } catch(e) {}
|
|
@@ -1389,6 +1726,13 @@
|
|
|
1389
1726
|
|
|
1390
1727
|
if (msg.type === "index-update") {
|
|
1391
1728
|
if (reindexBtn) reindexBtn.classList.remove("indexing");
|
|
1729
|
+
if (indexProgressPct) indexProgressPct.textContent = "100%";
|
|
1730
|
+
if (indexProgressFill) indexProgressFill.style.width = "100%";
|
|
1731
|
+
if (indexProgressText) indexProgressText.textContent = "Index ready";
|
|
1732
|
+
setTimeout(function() {
|
|
1733
|
+
if (indexProgressWrap) indexProgressWrap.classList.remove("active");
|
|
1734
|
+
}, 1200);
|
|
1735
|
+
|
|
1392
1736
|
var elapsed = msg.startMs ? (Date.now() - msg.startMs) : 0;
|
|
1393
1737
|
showToast("Indexed in " + elapsed + "ms", "✓");
|
|
1394
1738
|
if (ws && ws.readyState === WebSocket.OPEN) {
|