@dannote/figma-use 0.5.7 → 0.5.8
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 +14 -0
- package/SKILL.md +98 -0
- package/dist/cli/index.js +6 -2
- package/dist/proxy/index.js +10 -22
- package/package.json +1 -1
- package/packages/cli/src/render/reconciler.ts +2 -3
- package/packages/plugin/dist/main.js +27 -9
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.8] - 2026-01-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `page create` command documented in SKILL.md
|
|
15
|
+
- Auto-layout (hug contents) tests for render
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **JSX render hug contents** — auto-layout frames now correctly calculate size from children
|
|
20
|
+
- `trigger-layout` moved from proxy to CLI (ensures multiplayer nodes are visible)
|
|
21
|
+
- Plugin retries node lookup with exponential backoff
|
|
22
|
+
- Switching sizingMode FIXED→AUTO forces Figma to recalculate
|
|
23
|
+
|
|
10
24
|
## [0.5.7] - 2026-01-18
|
|
11
25
|
|
|
12
26
|
### Fixed
|
package/SKILL.md
CHANGED
|
@@ -51,6 +51,20 @@ echo '<Frame style={{padding: 24, gap: 16, flexDirection: "column", backgroundCo
|
|
|
51
51
|
|
|
52
52
|
**Style props:** `width`, `height`, `x`, `y`, `padding`, `paddingTop/Right/Bottom/Left`, `gap`, `flexDirection` (row|column), `justifyContent`, `alignItems`, `backgroundColor`, `borderColor`, `borderWidth`, `borderRadius`, `opacity`, `fontSize`, `fontFamily`, `fontWeight`, `color`, `textAlign`
|
|
53
53
|
|
|
54
|
+
### Auto-Layout (Hug Contents)
|
|
55
|
+
|
|
56
|
+
Frames with `flexDirection` automatically calculate size from children:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Height calculated as 50 + 10 (gap) + 30 = 90
|
|
60
|
+
echo '<Frame style={{width: 200, flexDirection: "column", gap: 10}}>
|
|
61
|
+
<Frame style={{width: 200, height: 50, backgroundColor: "#00FF00"}} />
|
|
62
|
+
<Frame style={{width: 200, height: 30, backgroundColor: "#0000FF"}} />
|
|
63
|
+
</Frame>' | figma-use render --stdin
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Limitation:** Row layout without explicit width collapses to 1×1 — always set `width` on row containers
|
|
67
|
+
|
|
54
68
|
### Buttons Example (3 sizes)
|
|
55
69
|
|
|
56
70
|
Since stdin doesn't support variables, write out each variant explicitly:
|
|
@@ -101,6 +115,7 @@ figma-use render ./MyComponent.figma.tsx
|
|
|
101
115
|
### Create
|
|
102
116
|
|
|
103
117
|
```bash
|
|
118
|
+
figma-use create page "Page Name"
|
|
104
119
|
figma-use create frame --width 400 --height 300 --fill "#FFF" --radius 12 --layout VERTICAL --gap 16
|
|
105
120
|
figma-use create rect --width 100 --height 50 --fill "#FF0000" --radius 8
|
|
106
121
|
figma-use create ellipse --width 80 --height 80 --fill "#00FF00"
|
|
@@ -217,3 +232,86 @@ Hex format: `#RGB`, `#RRGGBB`, `#RRGGBBAA`
|
|
|
217
232
|
## Node IDs
|
|
218
233
|
|
|
219
234
|
Format: `sessionID:localID` (e.g., `1:2`, `45:123`). Get from `figma-use selection get` or `figma-use node tree`.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Best Practices
|
|
239
|
+
|
|
240
|
+
### Always Verify Visually
|
|
241
|
+
|
|
242
|
+
After any operation, export a screenshot to confirm the result:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
figma-use export node <id> --scale 0.5 --output /tmp/check.png # Overview
|
|
246
|
+
figma-use export node <id> --scale 2 --output /tmp/detail.png # Details
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Copying Elements Between Pages
|
|
250
|
+
|
|
251
|
+
`node clone` creates a copy in the same parent. To move to another page:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
figma-use node clone <source-id> --json | jq -r '.id' # Get new ID
|
|
255
|
+
figma-use node set-parent <new-id> --parent <target-page-or-frame-id>
|
|
256
|
+
figma-use node move <new-id> --x 50 --y 50 # Reposition (coordinates reset)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Working with Sections
|
|
260
|
+
|
|
261
|
+
Sections organize components on a page. Elements must be explicitly moved inside:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
figma-use create section --name "Buttons" --x 0 --y 0 --width 600 --height 200
|
|
265
|
+
figma-use node set-parent <component-id> --parent <section-id>
|
|
266
|
+
figma-use node move <component-id> --x 50 --y 50 # Position inside section
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
⚠️ **Deleting a section deletes all children inside it!**
|
|
270
|
+
|
|
271
|
+
### Building a Component from Existing Design
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# 1. Find and copy the element
|
|
275
|
+
figma-use find --name "Button"
|
|
276
|
+
figma-use node clone <id> --json | jq -r '.id'
|
|
277
|
+
|
|
278
|
+
# 2. Move to components page/section
|
|
279
|
+
figma-use node set-parent <new-id> --parent <section-id>
|
|
280
|
+
figma-use node move <new-id> --x 50 --y 50
|
|
281
|
+
|
|
282
|
+
# 3. Rename with proper naming convention
|
|
283
|
+
figma-use node rename <new-id> "Button/Primary"
|
|
284
|
+
|
|
285
|
+
# 4. Convert to component
|
|
286
|
+
figma-use node to-component <new-id>
|
|
287
|
+
|
|
288
|
+
# 5. Verify
|
|
289
|
+
figma-use export node <section-id> --scale 0.5 --output /tmp/check.png
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Replacing Frames with Component Instances
|
|
293
|
+
|
|
294
|
+
When copying a composite element (like a dialog), nested elements are frames, not instances. To use existing components:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
# Delete the frame
|
|
298
|
+
figma-use node delete <frame-id>
|
|
299
|
+
|
|
300
|
+
# Create instance of the component
|
|
301
|
+
figma-use create instance --component <component-id> --x 50 --y 50 --parent <parent-id>
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Instance Element IDs
|
|
305
|
+
|
|
306
|
+
Elements inside instances have composite IDs: `I<instance-id>;<internal-id>`
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
figma-use set text "I123:456;789:10" "New text" # Modify text inside instance
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Finding Elements by Properties
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
figma-use find --type FRAME 2>&1 | grep "stroke: #EF4444" # Red border
|
|
316
|
+
figma-use find --type TEXT 2>&1 | grep "Bold" # Bold text
|
|
317
|
+
```
|
package/dist/cli/index.js
CHANGED
|
@@ -27952,7 +27952,7 @@ function styleToNodeChange(type, props, localID, sessionID, parentGUID, position
|
|
|
27952
27952
|
"flex-start": "MIN",
|
|
27953
27953
|
center: "CENTER",
|
|
27954
27954
|
"flex-end": "MAX",
|
|
27955
|
-
"space-
|
|
27955
|
+
"space-between": "SPACE_BETWEEN"
|
|
27956
27956
|
};
|
|
27957
27957
|
const mapped = validValues[style.justifyContent];
|
|
27958
27958
|
if (mapped) {
|
|
@@ -28605,6 +28605,10 @@ var render_default = defineCommand({
|
|
|
28605
28605
|
const pendingInstances = getPendingComponentSetInstances();
|
|
28606
28606
|
clearPendingComponentSetInstances();
|
|
28607
28607
|
await sendNodeChanges(result.nodeChanges, pendingInstances);
|
|
28608
|
+
const rootId = `${result.nodeChanges[0].guid.sessionID}:${result.nodeChanges[0].guid.localID}`;
|
|
28609
|
+
try {
|
|
28610
|
+
await sendCommand("trigger-layout", { nodeId: rootId, pendingComponentSetInstances: pendingInstances });
|
|
28611
|
+
} catch {}
|
|
28608
28612
|
if (args.json) {
|
|
28609
28613
|
const ids = result.nodeChanges.map((nc) => ({
|
|
28610
28614
|
id: `${nc.guid.sessionID}:${nc.guid.localID}`,
|
|
@@ -29146,7 +29150,7 @@ var set_parent_default = defineCommand({
|
|
|
29146
29150
|
},
|
|
29147
29151
|
async run({ args }) {
|
|
29148
29152
|
try {
|
|
29149
|
-
const result = await sendCommand("set-parent", {
|
|
29153
|
+
const result = await sendCommand("set-parent-id", {
|
|
29150
29154
|
id: args.id,
|
|
29151
29155
|
parentId: args.parent,
|
|
29152
29156
|
index: args.index ? Number(args.index) : undefined
|
package/dist/proxy/index.js
CHANGED
|
@@ -178409,7 +178409,7 @@ enum StackJustify {
|
|
|
178409
178409
|
MIN = 0;
|
|
178410
178410
|
CENTER = 1;
|
|
178411
178411
|
MAX = 2;
|
|
178412
|
-
|
|
178412
|
+
SPACE_BETWEEN = 3;
|
|
178413
178413
|
}
|
|
178414
178414
|
|
|
178415
178415
|
enum StackSize {
|
|
@@ -209429,27 +209429,15 @@ new Elysia().ws("/plugin", {
|
|
|
209429
209429
|
try {
|
|
209430
209430
|
const { client, sessionID } = await getMultiplayerConnection(fileKey);
|
|
209431
209431
|
consola.info(`render: ${nodeChanges.length} nodes to ${fileKey}`);
|
|
209432
|
-
|
|
209433
|
-
|
|
209434
|
-
|
|
209435
|
-
const
|
|
209436
|
-
|
|
209437
|
-
|
|
209438
|
-
|
|
209439
|
-
|
|
209440
|
-
|
|
209441
|
-
}, 5000);
|
|
209442
|
-
pendingRequests.set(layoutId, { resolve: () => resolve(), reject, timeout });
|
|
209443
|
-
sendToPlugin(JSON.stringify({
|
|
209444
|
-
id: layoutId,
|
|
209445
|
-
command: "trigger-layout",
|
|
209446
|
-
args: {
|
|
209447
|
-
nodeId: rootId,
|
|
209448
|
-
pendingComponentSetInstances: body.pendingComponentSetInstances || []
|
|
209449
|
-
}
|
|
209450
|
-
}));
|
|
209451
|
-
});
|
|
209452
|
-
} catch {}
|
|
209432
|
+
try {
|
|
209433
|
+
await client.sendNodeChangesSync(nodeChanges);
|
|
209434
|
+
} catch (codecError) {
|
|
209435
|
+
const msg = codecError instanceof Error ? codecError.message : String(codecError);
|
|
209436
|
+
if (msg.includes("Invalid value") && msg.includes("for enum")) {
|
|
209437
|
+
consola.error("Codec error:", msg);
|
|
209438
|
+
return { error: `Unsupported value in node properties: ${msg}` };
|
|
209439
|
+
}
|
|
209440
|
+
throw codecError;
|
|
209453
209441
|
}
|
|
209454
209442
|
const ids = nodeChanges.map((nc) => ({
|
|
209455
209443
|
id: `${nc.guid.sessionID}:${nc.guid.localID}`,
|
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* ### Auto-layout field names differ from Plugin API
|
|
16
16
|
* - justifyContent → stackPrimaryAlignItems (not stackJustify)
|
|
17
17
|
* - alignItems → stackCounterAlignItems (not stackCounterAlign)
|
|
18
|
-
* - Valid values: 'MIN', 'CENTER', 'MAX', '
|
|
18
|
+
* - Valid values: 'MIN', 'CENTER', 'MAX', 'SPACE_BETWEEN', 'SPACE_EVENLY'
|
|
19
19
|
*
|
|
20
20
|
* ### Sizing modes for auto-layout
|
|
21
21
|
* - stackPrimarySizing/stackCounterSizing = 'FIXED' when explicit size given
|
|
@@ -247,10 +247,9 @@ function styleToNodeChange(
|
|
|
247
247
|
// Alignment - NOTE: field names differ from Plugin API!
|
|
248
248
|
// Plugin API uses primaryAxisAlignItems/counterAxisAlignItems
|
|
249
249
|
// Multiplayer uses stackPrimaryAlignItems/stackCounterAlignItems
|
|
250
|
-
// Also: 'SPACE_BETWEEN' doesn't exist in multiplayer, only 'SPACE_EVENLY'
|
|
251
250
|
if (style.justifyContent) {
|
|
252
251
|
const validValues: Record<string, string> = {
|
|
253
|
-
'flex-start': 'MIN', 'center': 'CENTER', 'flex-end': 'MAX', 'space-
|
|
252
|
+
'flex-start': 'MIN', 'center': 'CENTER', 'flex-end': 'MAX', 'space-between': 'SPACE_BETWEEN'
|
|
254
253
|
}
|
|
255
254
|
const mapped = validValues[style.justifyContent as string]
|
|
256
255
|
if (mapped) {
|
|
@@ -235,7 +235,8 @@
|
|
|
235
235
|
parent.appendChild(attachment.node);
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
|
-
for (
|
|
238
|
+
for (let i = deferredLayouts.length - 1; i >= 0; i--) {
|
|
239
|
+
const layout = deferredLayouts[i];
|
|
239
240
|
layout.frame.layoutMode = layout.layoutMode;
|
|
240
241
|
layout.frame.primaryAxisSizingMode = "AUTO";
|
|
241
242
|
layout.frame.counterAxisSizingMode = "AUTO";
|
|
@@ -642,8 +643,14 @@
|
|
|
642
643
|
case "resize-node": {
|
|
643
644
|
const { id, width, height } = args;
|
|
644
645
|
const node = yield figma.getNodeByIdAsync(id);
|
|
645
|
-
if (!node
|
|
646
|
-
|
|
646
|
+
if (!node) throw new Error("Node not found");
|
|
647
|
+
if ("resize" in node) {
|
|
648
|
+
node.resize(width, height);
|
|
649
|
+
} else if ("width" in node && "height" in node) {
|
|
650
|
+
node.resizeWithoutConstraints(width, height);
|
|
651
|
+
} else {
|
|
652
|
+
throw new Error("Node cannot be resized");
|
|
653
|
+
}
|
|
647
654
|
return serializeNode(node);
|
|
648
655
|
}
|
|
649
656
|
// ==================== UPDATE APPEARANCE ====================
|
|
@@ -1211,7 +1218,12 @@
|
|
|
1211
1218
|
// ==================== LAYOUT ====================
|
|
1212
1219
|
case "trigger-layout": {
|
|
1213
1220
|
const { nodeId, pendingComponentSetInstances } = args;
|
|
1214
|
-
|
|
1221
|
+
let root = null;
|
|
1222
|
+
for (let i = 0; i < 10; i++) {
|
|
1223
|
+
root = yield figma.getNodeByIdAsync(nodeId);
|
|
1224
|
+
if (root) break;
|
|
1225
|
+
yield new Promise((r) => setTimeout(r, 100 * (i + 1)));
|
|
1226
|
+
}
|
|
1215
1227
|
if (!root) return null;
|
|
1216
1228
|
if (pendingComponentSetInstances && pendingComponentSetInstances.length > 0) {
|
|
1217
1229
|
for (const pending of pendingComponentSetInstances) {
|
|
@@ -1260,11 +1272,17 @@
|
|
|
1260
1272
|
}
|
|
1261
1273
|
if ("layoutMode" in node && node.layoutMode !== "NONE") {
|
|
1262
1274
|
const frame = node;
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1275
|
+
const needsPrimaryRecalc = frame.primaryAxisSizingMode === "AUTO";
|
|
1276
|
+
const needsCounterRecalc = frame.counterAxisSizingMode === "AUTO";
|
|
1277
|
+
if (needsPrimaryRecalc || needsCounterRecalc) {
|
|
1278
|
+
if (needsPrimaryRecalc) {
|
|
1279
|
+
frame.primaryAxisSizingMode = "FIXED";
|
|
1280
|
+
frame.primaryAxisSizingMode = "AUTO";
|
|
1281
|
+
}
|
|
1282
|
+
if (needsCounterRecalc) {
|
|
1283
|
+
frame.counterAxisSizingMode = "FIXED";
|
|
1284
|
+
frame.counterAxisSizingMode = "AUTO";
|
|
1285
|
+
}
|
|
1268
1286
|
}
|
|
1269
1287
|
}
|
|
1270
1288
|
});
|