@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.
- package/CHANGELOG.md +100 -0
- package/README.md +217 -334
- package/SKILL.md +78 -1
- package/bin/figma-use.js +4 -1
- package/dist/cli/index.js +563 -83
- package/dist/proxy/index.js +180 -2
- package/package.json +1 -1
- package/packages/plugin/dist/main.js +93 -1
package/dist/proxy/index.js
CHANGED
|
@@ -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
|
|
11573
|
-
|
|
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
|
@@ -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();
|