@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 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-evenly": "SPACE_EVENLY"
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
@@ -178409,7 +178409,7 @@ enum StackJustify {
178409
178409
  MIN = 0;
178410
178410
  CENTER = 1;
178411
178411
  MAX = 2;
178412
- SPACE_EVENLY = 3;
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
- await client.sendNodeChangesSync(nodeChanges);
209433
- if (sendToPlugin) {
209434
- const rootId = `${nodeChanges[0].guid.sessionID}:${nodeChanges[0].guid.localID}`;
209435
- const layoutId = crypto.randomUUID();
209436
- try {
209437
- await new Promise((resolve, reject) => {
209438
- const timeout = setTimeout(() => {
209439
- pendingRequests.delete(layoutId);
209440
- reject(new Error("Layout trigger timeout"));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dannote/figma-use",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "Control Figma from the command line. Full read/write access for AI agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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', 'SPACE_EVENLY' (not 'SPACE_BETWEEN')
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-evenly': 'SPACE_EVENLY'
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 (const layout of deferredLayouts) {
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 || !("resize" in node)) throw new Error("Node not found");
646
- node.resize(width, height);
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
- const root = yield figma.getNodeByIdAsync(nodeId);
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
- if (frame.width <= 1 && frame.height <= 1) {
1264
- frame.primaryAxisSizingMode = "AUTO";
1265
- frame.counterAxisSizingMode = "AUTO";
1266
- frame.resize(1.01, 1.01);
1267
- frame.resize(1, 1);
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
  });