@ttsalpha/mirra 1.0.0 → 1.2.0

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.
@@ -837,6 +837,11 @@
837
837
  // src/ops/pages.ts
838
838
  async function pages(params) {
839
839
  await figma.loadAllPagesAsync();
840
+ if (params.create) {
841
+ const page = figma.createPage();
842
+ page.name = params.create;
843
+ await figma.setCurrentPageAsync(page);
844
+ }
840
845
  if (params.select) {
841
846
  const wanted = figma.root.children.find(
842
847
  (page) => page.id === params.select || page.name === params.select
@@ -895,6 +900,18 @@
895
900
  touched.push(created);
896
901
  break;
897
902
  }
903
+ case "component": {
904
+ const key = node.getPluginData(KEY_FIELD);
905
+ const component = figma.createComponentFromNode(node);
906
+ component.name = op.name ?? node.name;
907
+ if (key) {
908
+ component.setPluginData(KEY_FIELD, key);
909
+ keys[key] = component.id;
910
+ }
911
+ index.invalidate();
912
+ touched.push(component);
913
+ break;
914
+ }
898
915
  case "move": {
899
916
  const parent = op.parent ? await index.resolve(op.parent) : node.parent;
900
917
  if (!parent || !("insertChild" in parent)) {
@@ -932,30 +949,39 @@
932
949
  return node;
933
950
  }
934
951
 
952
+ // src/version.ts
953
+ var PLUGIN_VERSION = "1.2.0";
954
+
935
955
  // src/ops/ping.ts
936
956
  function ping() {
937
957
  return {
938
958
  fileName: figma.root.name,
939
959
  fileKey: readFileKey(),
940
960
  currentPage: figma.currentPage.name,
941
- selectionCount: figma.currentPage.selection.length
961
+ selectionCount: figma.currentPage.selection.length,
962
+ pluginVersion: PLUGIN_VERSION
942
963
  };
943
964
  }
944
965
 
945
966
  // src/figma/read.ts
946
- function readNode(node, depth, names = EMPTY_NAMES) {
947
- const skipped = { count: 0 };
948
- const tree = toSpec(node, depth, skipped, names);
949
- return { tree, skipped: skipped.count };
950
- }
951
- function toSpec(node, depth, skipped, names) {
967
+ function readNode(node, depth, names = EMPTY_NAMES, limit = Number.MAX_SAFE_INTEGER) {
968
+ const budget = { count: 0, left: limit };
969
+ const tree = toSpec(node, depth, budget, names);
970
+ return { tree, skipped: budget.count };
971
+ }
972
+ function toSpec(node, depth, budget, names) {
973
+ if (budget.left <= 0) {
974
+ budget.count += 1;
975
+ return null;
976
+ }
977
+ budget.left -= 1;
952
978
  const svg = node.getPluginData(SVG_FIELD);
953
979
  if (svg) return { type: "svg", ...common(node), src: svg };
954
980
  const src = node.getPluginData(SRC_FIELD);
955
981
  if (src) return { type: "image", ...common(node), ...painted(node, names), src };
956
982
  switch (node.type) {
957
983
  case "FRAME":
958
- return frameSpec(node, depth, skipped, names);
984
+ return frameSpec(node, depth, budget, names);
959
985
  case "TEXT":
960
986
  return textSpec(node, names);
961
987
  case "RECTANGLE":
@@ -965,19 +991,19 @@
965
991
  case "INSTANCE":
966
992
  return instanceSpec(node);
967
993
  default:
968
- skipped.count += 1;
994
+ budget.count += 1;
969
995
  return null;
970
996
  }
971
997
  }
972
- function frameSpec(node, depth, skipped, names) {
998
+ function frameSpec(node, depth, budget, names) {
973
999
  const children = [];
974
1000
  if (depth > 0) {
975
1001
  for (const child of node.children) {
976
- const spec = toSpec(child, depth - 1, skipped, names);
1002
+ const spec = toSpec(child, depth - 1, budget, names);
977
1003
  if (spec) children.push(spec);
978
1004
  }
979
1005
  } else if (node.children.length) {
980
- skipped.count += node.children.length;
1006
+ budget.count += node.children.length;
981
1007
  }
982
1008
  return {
983
1009
  type: "frame",
@@ -1134,6 +1160,7 @@
1134
1160
 
1135
1161
  // src/ops/read.ts
1136
1162
  var DEFAULT_DEPTH = 6;
1163
+ var DEFAULT_LIMIT2 = 200;
1137
1164
  async function read(params) {
1138
1165
  const targets = params.nodeId ? [await resolve(params.nodeId)] : [...figma.currentPage.selection];
1139
1166
  if (!targets.length) {
@@ -1143,10 +1170,14 @@
1143
1170
  const nodes = [];
1144
1171
  let skipped = 0;
1145
1172
  for (const target of targets) {
1146
- const outcome = readNode(target, params.depth ?? DEFAULT_DEPTH, names);
1173
+ const outcome = readNode(
1174
+ target,
1175
+ params.depth ?? DEFAULT_DEPTH,
1176
+ names,
1177
+ params.limit ?? DEFAULT_LIMIT2
1178
+ );
1147
1179
  skipped += outcome.skipped;
1148
1180
  if (outcome.tree) nodes.push({ id: target.id, tree: outcome.tree });
1149
- else skipped += 1;
1150
1181
  }
1151
1182
  return { nodes, skipped };
1152
1183
  }
@@ -1194,21 +1225,29 @@
1194
1225
  }
1195
1226
 
1196
1227
  // src/ops/screenshot.ts
1228
+ var DEFAULT_MAX_EDGE = 1200;
1229
+ function fittingScale(wanted, box, maxEdge) {
1230
+ const longest = Math.max(box.width, box.height) * wanted;
1231
+ if (longest <= maxEdge) return wanted;
1232
+ return Math.max(0.01, wanted * maxEdge / longest);
1233
+ }
1197
1234
  async function screenshot(params) {
1198
1235
  const node = params.target ? await new NodeIndex().resolve(params.target) : figma.currentPage.selection[0];
1199
1236
  if (!node) {
1200
1237
  throw new Error("Nothing is selected: select a layer in Figma, or pass a target");
1201
1238
  }
1239
+ const box = node;
1240
+ const scale = fittingScale(params.scale ?? 1, box, params.maxEdge ?? DEFAULT_MAX_EDGE);
1202
1241
  const bytes = await node.exportAsync({
1203
1242
  format: "PNG",
1204
- constraint: { type: "SCALE", value: params.scale ?? 1 }
1243
+ constraint: { type: "SCALE", value: scale }
1205
1244
  });
1206
- const box = node;
1207
1245
  return {
1208
1246
  id: node.id,
1209
1247
  name: node.name,
1210
- width: Math.round(box.width),
1211
- height: Math.round(box.height),
1248
+ width: Math.round(box.width * scale),
1249
+ height: Math.round(box.height * scale),
1250
+ scale,
1212
1251
  png: figma.base64Encode(bytes)
1213
1252
  };
1214
1253
  }
@@ -1252,7 +1291,6 @@
1252
1291
  }
1253
1292
 
1254
1293
  // src/main.ts
1255
- var PLUGIN_VERSION = "1.0.0";
1256
1294
  figma.showUI(__html__, { width: 300, height: 300, themeColors: true });
1257
1295
  var queue = Promise.resolve();
1258
1296
  figma.ui.onmessage = (message2) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsalpha/mirra",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Two-way MCP server for Figma: read a design and render one back through the Figma Plugin API",
5
5
  "keywords": [
6
6
  "figma",
@@ -39,17 +39,16 @@
39
39
  "prepublishOnly": "pnpm -w run build",
40
40
  "build:mcpb": "node scripts/build-mcpb.mjs"
41
41
  },
42
- "dependencies": {
43
- "@modelcontextprotocol/sdk": "1.32.1",
44
- "ws": "8.22.0",
45
- "zod": "4.6.7"
46
- },
42
+ "dependencies": {},
47
43
  "devDependencies": {
48
44
  "@mirra/plugin": "workspace:*",
49
45
  "@mirra/schema": "workspace:*",
46
+ "@modelcontextprotocol/sdk": "1.32.1",
50
47
  "@types/node": "24.19.2",
51
48
  "@types/ws": "8.18.2",
52
- "esbuild": "0.25.12"
49
+ "esbuild": "0.25.12",
50
+ "ws": "8.22.0",
51
+ "zod": "4.6.7"
53
52
  },
54
53
  "publishConfig": {
55
54
  "access": "public"