@dannote/figma-use 0.3.0 → 0.5.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.
@@ -10178,6 +10178,21 @@ message PaintFilterMessage {
10178
10178
  float brightness = 10 [deprecated];
10179
10179
  }
10180
10180
 
10181
+ // Variable binding embedded in Paint - discovered via WS sniffing 2026-01
10182
+ // Figma wire format: 15 01 04 01 {sessionID varint} {localID varint}
10183
+ // - 15 = field 21 in Paint
10184
+ // - 01 = always 1 (binding type for color)
10185
+ // - 04 = field 4
10186
+ // - 01 = always 1 (flag)
10187
+ // - then GUID as raw sessionID + localID varints (no field numbers)
10188
+ // NOTE: kiwi-schema cannot produce this exact format, handled manually in codec
10189
+ message PaintVariableBinding {
10190
+ uint bindingType = 1; // Always 1 for color
10191
+ uint flag = 2; // Placeholder
10192
+ uint flag2 = 3; // Placeholder
10193
+ GUID variableID = 4; // Variable GUID
10194
+ }
10195
+
10181
10196
  message Paint {
10182
10197
  PaintType type = 1;
10183
10198
  Color color = 2;
@@ -10199,6 +10214,7 @@ message Paint {
10199
10214
  Video video = 18;
10200
10215
  uint originalImageWidth = 19;
10201
10216
  uint originalImageHeight = 20;
10217
+ PaintVariableBinding variableBinding = 21; // Not in fig-kiwi. Discovered via WS sniffing 2026-01
10202
10218
  }
10203
10219
 
10204
10220
  message FontMetaData {
@@ -11320,6 +11336,8 @@ message Message {
11320
11336
  uint reconnectSequenceNumber = 25;
11321
11337
  string pasteBranchSourceFileKey = 26;
11322
11338
  EditorType pasteEditorType = 27;
11339
+ // Field 38 (timestamp) exists but kiwi-schema limits field IDs to 28
11340
+ // Discovered via WS sniffing 2026-01, skipped during manual pre-processing
11323
11341
  }
11324
11342
 
11325
11343
  message DiffChunk {
@@ -11540,8 +11558,11 @@ var init_schema = __esm(() => {
11540
11558
  var exports_codec = {};
11541
11559
  __export(exports_codec, {
11542
11560
  peekMessageType: () => peekMessageType,
11561
+ parseVariableId: () => parseVariableId,
11543
11562
  isCodecReady: () => isCodecReady,
11544
11563
  initCodec: () => initCodec,
11564
+ encodePaintWithVariableBinding: () => encodePaintWithVariableBinding,
11565
+ encodeNodeChangeWithVariables: () => encodeNodeChangeWithVariables,
11545
11566
  encodeMessage: () => encodeMessage,
11546
11567
  decompress: () => decompress,
11547
11568
  decodeMessage: () => decodeMessage,
@@ -11569,8 +11590,36 @@ function encodeMessage(message) {
11569
11590
  if (!compiledSchema) {
11570
11591
  throw new Error("Codec not initialized. Call initCodec() first.");
11571
11592
  }
11572
- const encoded = compiledSchema.encodeMessage(message);
11573
- return compress(encoded);
11593
+ const hasVariables = message.nodeChanges?.some((nc) => nc.fillPaints?.some((p4) => p4.colorVariableBinding) || nc.strokePaints?.some((p4) => p4.colorVariableBinding));
11594
+ if (!hasVariables) {
11595
+ const encoded = compiledSchema.encodeMessage(message);
11596
+ return compress(encoded);
11597
+ }
11598
+ const messageWithoutNodes = { ...message, nodeChanges: [] };
11599
+ const baseEncoded = compiledSchema.encodeMessage(messageWithoutNodes);
11600
+ const baseHex = Buffer.from(baseEncoded).toString("hex");
11601
+ const nodeChangeBytes = [];
11602
+ for (const nc of message.nodeChanges || []) {
11603
+ const encoded = encodeNodeChangeWithVariables(nc);
11604
+ nodeChangeBytes.push(encoded);
11605
+ }
11606
+ const emptyArrayPattern = "0400";
11607
+ const emptyArrayIdx = baseHex.indexOf(emptyArrayPattern);
11608
+ if (emptyArrayIdx === -1) {
11609
+ const encoded = compiledSchema.encodeMessage(message);
11610
+ return compress(encoded);
11611
+ }
11612
+ const ncBytes = [4];
11613
+ ncBytes.push(...encodeVarint(nodeChangeBytes.length));
11614
+ for (const ncArr of nodeChangeBytes) {
11615
+ ncBytes.push(...Array.from(ncArr));
11616
+ }
11617
+ const beforeArray = baseHex.slice(0, emptyArrayIdx);
11618
+ const afterArray = baseHex.slice(emptyArrayIdx + 4);
11619
+ const ncHex = Buffer.from(ncBytes).toString("hex");
11620
+ const finalHex = beforeArray + ncHex + afterArray;
11621
+ const finalBytes = new Uint8Array(finalHex.match(/.{2}/g).map((b2) => parseInt(b2, 16)));
11622
+ return compress(finalBytes);
11574
11623
  }
11575
11624
  function decodeMessage(data) {
11576
11625
  if (!compiledSchema) {
@@ -11644,6 +11693,114 @@ function createNodeChange(opts) {
11644
11693
  }
11645
11694
  return change;
11646
11695
  }
11696
+ function encodeVarint(value) {
11697
+ const bytes = [];
11698
+ while (value > 127) {
11699
+ bytes.push(value & 127 | 128);
11700
+ value >>>= 7;
11701
+ }
11702
+ bytes.push(value);
11703
+ return bytes;
11704
+ }
11705
+ function encodePaintWithVariableBinding(paint, variableSessionID, variableLocalID) {
11706
+ if (!compiledSchema) {
11707
+ throw new Error("Codec not initialized. Call initCodec() first.");
11708
+ }
11709
+ const basePaint = { ...paint };
11710
+ delete basePaint.colorVariableBinding;
11711
+ const baseBytes = compiledSchema.encodePaint(basePaint);
11712
+ const baseArray = Array.from(baseBytes);
11713
+ if (baseArray[baseArray.length - 1] === 0) {
11714
+ baseArray.pop();
11715
+ }
11716
+ baseArray.push(21, 1);
11717
+ baseArray.push(4, 1);
11718
+ baseArray.push(...encodeVarint(variableSessionID));
11719
+ baseArray.push(...encodeVarint(variableLocalID));
11720
+ baseArray.push(0, 0, 2, 3, 3, 4);
11721
+ baseArray.push(0, 0);
11722
+ return new Uint8Array(baseArray);
11723
+ }
11724
+ function parseVariableId(variableId) {
11725
+ const match = variableId.match(/VariableID:(\d+):(\d+)/);
11726
+ if (!match)
11727
+ return null;
11728
+ return {
11729
+ sessionID: parseInt(match[1], 10),
11730
+ localID: parseInt(match[2], 10)
11731
+ };
11732
+ }
11733
+ function encodeNodeChangeWithVariables(nodeChange) {
11734
+ if (!compiledSchema) {
11735
+ throw new Error("Codec not initialized. Call initCodec() first.");
11736
+ }
11737
+ const hasFillBinding = nodeChange.fillPaints?.some((p4) => p4.colorVariableBinding);
11738
+ const hasStrokeBinding = nodeChange.strokePaints?.some((p4) => p4.colorVariableBinding);
11739
+ if (!hasFillBinding && !hasStrokeBinding) {
11740
+ return compiledSchema.encodeNodeChange(nodeChange);
11741
+ }
11742
+ const cleanNodeChange = { ...nodeChange };
11743
+ if (cleanNodeChange.fillPaints) {
11744
+ cleanNodeChange.fillPaints = cleanNodeChange.fillPaints.map((p4) => {
11745
+ const clean2 = { ...p4 };
11746
+ delete clean2.colorVariableBinding;
11747
+ return clean2;
11748
+ });
11749
+ }
11750
+ if (cleanNodeChange.strokePaints) {
11751
+ cleanNodeChange.strokePaints = cleanNodeChange.strokePaints.map((p4) => {
11752
+ const clean2 = { ...p4 };
11753
+ delete clean2.colorVariableBinding;
11754
+ return clean2;
11755
+ });
11756
+ }
11757
+ const baseBytes = compiledSchema.encodeNodeChange(cleanNodeChange);
11758
+ let hex2 = Buffer.from(baseBytes).toString("hex");
11759
+ if (hasFillBinding && nodeChange.fillPaints?.[0]?.colorVariableBinding) {
11760
+ hex2 = injectVariableBinding(hex2, "2601", nodeChange.fillPaints[0].colorVariableBinding);
11761
+ }
11762
+ if (hasStrokeBinding && nodeChange.strokePaints?.[0]?.colorVariableBinding) {
11763
+ hex2 = injectVariableBinding(hex2, "2701", nodeChange.strokePaints[0].colorVariableBinding);
11764
+ }
11765
+ return new Uint8Array(hex2.match(/.{2}/g).map((b2) => parseInt(b2, 16)));
11766
+ }
11767
+ function injectVariableBinding(hex2, marker, binding) {
11768
+ const markerIdx = hex2.indexOf(marker);
11769
+ if (markerIdx === -1)
11770
+ return hex2;
11771
+ const visiblePattern = "0401";
11772
+ let patternIdx = hex2.indexOf(visiblePattern, markerIdx);
11773
+ if (patternIdx === -1)
11774
+ return hex2;
11775
+ let insertPoint = patternIdx + visiblePattern.length;
11776
+ if (hex2.slice(insertPoint, insertPoint + 4) === "0501") {
11777
+ insertPoint += 4;
11778
+ }
11779
+ const varBytes = [
11780
+ 21,
11781
+ 1,
11782
+ 4,
11783
+ 1,
11784
+ ...encodeVarint(binding.variableID.sessionID),
11785
+ ...encodeVarint(binding.variableID.localID),
11786
+ 0,
11787
+ 0,
11788
+ 2,
11789
+ 3,
11790
+ 3,
11791
+ 4,
11792
+ 0,
11793
+ 0
11794
+ ];
11795
+ const varHex = Buffer.from(varBytes).toString("hex");
11796
+ const beforeVar = hex2.slice(0, insertPoint);
11797
+ let afterIdx = insertPoint;
11798
+ if (hex2.slice(afterIdx, afterIdx + 2) === "00") {
11799
+ afterIdx += 2;
11800
+ }
11801
+ const afterVar = hex2.slice(afterIdx);
11802
+ return beforeVar + varHex + afterVar;
11803
+ }
11647
11804
  var compiledSchema = null;
11648
11805
  var init_codec = __esm(() => {
11649
11806
  init_kiwi_esm();
@@ -26796,6 +26953,27 @@ new Elysia().ws("/plugin", {
26796
26953
  const { client, sessionID } = await getMultiplayerConnection(fileKey);
26797
26954
  consola.info(`render: ${nodeChanges.length} nodes to ${fileKey}`);
26798
26955
  await client.sendNodeChangesSync(nodeChanges);
26956
+ if (sendToPlugin) {
26957
+ const rootId = `${nodeChanges[0].guid.sessionID}:${nodeChanges[0].guid.localID}`;
26958
+ const layoutId = crypto.randomUUID();
26959
+ try {
26960
+ await new Promise((resolve, reject) => {
26961
+ const timeout = setTimeout(() => {
26962
+ pendingRequests.delete(layoutId);
26963
+ reject(new Error("Layout trigger timeout"));
26964
+ }, 5000);
26965
+ pendingRequests.set(layoutId, { resolve: () => resolve(), reject, timeout });
26966
+ sendToPlugin(JSON.stringify({
26967
+ id: layoutId,
26968
+ command: "trigger-layout",
26969
+ args: {
26970
+ nodeId: rootId,
26971
+ pendingComponentSetInstances: body.pendingComponentSetInstances || []
26972
+ }
26973
+ }));
26974
+ });
26975
+ } catch {}
26976
+ }
26799
26977
  const ids = nodeChanges.map((nc) => ({
26800
26978
  id: `${nc.guid.sessionID}:${nc.guid.localID}`,
26801
26979
  name: nc.name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dannote/figma-use",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Control Figma from the command line. Full read/write access for AI agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1184,10 +1184,76 @@
1184
1184
  const fn = new AsyncFunction("figma", wrappedCode);
1185
1185
  return yield fn(figma);
1186
1186
  }
1187
+ // ==================== LAYOUT ====================
1188
+ case "trigger-layout": {
1189
+ const { nodeId, pendingComponentSetInstances } = args;
1190
+ const root = yield figma.getNodeByIdAsync(nodeId);
1191
+ if (!root) return null;
1192
+ if (pendingComponentSetInstances && pendingComponentSetInstances.length > 0) {
1193
+ for (const pending of pendingComponentSetInstances) {
1194
+ const findComponentSet = (node) => {
1195
+ if (node.type === "COMPONENT_SET" && node.name === pending.componentSetName) {
1196
+ return node;
1197
+ }
1198
+ if ("children" in node) {
1199
+ for (const child of node.children) {
1200
+ const found = findComponentSet(child);
1201
+ if (found) return found;
1202
+ }
1203
+ }
1204
+ return null;
1205
+ };
1206
+ const componentSet = findComponentSet(root);
1207
+ if (!componentSet) continue;
1208
+ const variantComp = componentSet.children.find(
1209
+ (c) => c.type === "COMPONENT" && c.name === pending.variantName
1210
+ );
1211
+ if (!variantComp) continue;
1212
+ const instance = variantComp.createInstance();
1213
+ instance.x = pending.x;
1214
+ instance.y = pending.y;
1215
+ const parentId = `${pending.parentGUID.sessionID}:${pending.parentGUID.localID}`;
1216
+ const parent = yield figma.getNodeByIdAsync(parentId);
1217
+ if (parent && "appendChild" in parent) {
1218
+ parent.appendChild(instance);
1219
+ }
1220
+ }
1221
+ }
1222
+ const fixRecursive = (node) => __async(null, null, function* () {
1223
+ if ("children" in node) {
1224
+ for (const child of node.children) {
1225
+ yield fixRecursive(child);
1226
+ }
1227
+ }
1228
+ if (node.type === "TEXT" && node.textAutoResize !== "NONE") {
1229
+ try {
1230
+ yield figma.loadFontAsync(node.fontName);
1231
+ const chars = node.characters;
1232
+ node.characters = "";
1233
+ node.characters = chars;
1234
+ } catch (e) {
1235
+ }
1236
+ }
1237
+ if ("layoutMode" in node && node.layoutMode !== "NONE") {
1238
+ const frame = node;
1239
+ if (frame.width <= 1 && frame.height <= 1) {
1240
+ frame.primaryAxisSizingMode = "AUTO";
1241
+ frame.counterAxisSizingMode = "AUTO";
1242
+ frame.resize(1.01, 1.01);
1243
+ frame.resize(1, 1);
1244
+ }
1245
+ }
1246
+ });
1247
+ yield fixRecursive(root);
1248
+ return { triggered: true };
1249
+ }
1187
1250
  // ==================== VARIABLES ====================
1188
1251
  case "get-variables": {
1189
- const { type } = args;
1252
+ const { type, simple } = args;
1190
1253
  const variables = yield figma.variables.getLocalVariablesAsync(type);
1254
+ if (simple) {
1255
+ return variables.map((v) => ({ id: v.id, name: v.name }));
1256
+ }
1191
1257
  return variables.map((v) => serializeVariable(v));
1192
1258
  }
1193
1259
  case "get-variable": {
@@ -1234,6 +1300,32 @@
1234
1300
  }
1235
1301
  return serializeNode(node);
1236
1302
  }
1303
+ case "bind-fill-variable": {
1304
+ const { nodeId, variableId, paintIndex = 0 } = args;
1305
+ const node = yield figma.getNodeByIdAsync(nodeId);
1306
+ if (!node) throw new Error("Node not found");
1307
+ if (!("fills" in node)) throw new Error("Node does not have fills");
1308
+ const variable = yield figma.variables.getVariableByIdAsync(variableId);
1309
+ if (!variable) throw new Error("Variable not found");
1310
+ const fills = node.fills;
1311
+ if (!fills[paintIndex]) throw new Error("Paint not found at index " + paintIndex);
1312
+ const newFill = figma.variables.setBoundVariableForPaint(fills[paintIndex], "color", variable);
1313
+ node.fills = [...fills.slice(0, paintIndex), newFill, ...fills.slice(paintIndex + 1)];
1314
+ return serializeNode(node);
1315
+ }
1316
+ case "bind-stroke-variable": {
1317
+ const { nodeId, variableId, paintIndex = 0 } = args;
1318
+ const node = yield figma.getNodeByIdAsync(nodeId);
1319
+ if (!node) throw new Error("Node not found");
1320
+ if (!("strokes" in node)) throw new Error("Node does not have strokes");
1321
+ const variable = yield figma.variables.getVariableByIdAsync(variableId);
1322
+ if (!variable) throw new Error("Variable not found");
1323
+ const strokes = node.strokes;
1324
+ if (!strokes[paintIndex]) throw new Error("Paint not found at index " + paintIndex);
1325
+ const newStroke = figma.variables.setBoundVariableForPaint(strokes[paintIndex], "color", variable);
1326
+ node.strokes = [...strokes.slice(0, paintIndex), newStroke, ...strokes.slice(paintIndex + 1)];
1327
+ return serializeNode(node);
1328
+ }
1237
1329
  // ==================== VARIABLE COLLECTIONS ====================
1238
1330
  case "get-variable-collections": {
1239
1331
  const collections = yield figma.variables.getLocalVariableCollectionsAsync();