figma-local 1.4.0 → 1.6.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +146 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-local",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Control Figma Desktop with Claude Code. Smart read, write, and AI-prompt export. No API key required.",
5
5
  "author": "elvke",
6
6
  "license": "MIT",
package/src/index.js CHANGED
@@ -8537,6 +8537,7 @@ program
8537
8537
  .option('--tokens', 'Show only the design tokens (colors, spacing, etc.) used in the frame')
8538
8538
  .option('--selection', 'Read whatever is currently selected in Figma')
8539
8539
  .option('--link <url>', 'Read a specific node from a Figma selection link (e.g. copied via "Copy link to selection")')
8540
+ .option('--page', 'Read the entire current page — all frames with full structure, tokens, and specs')
8540
8541
  .option('--stage <n>', 'Run a specific stage: 1 (list frames), 2 (structure), 3 (tokens)')
8541
8542
  .addHelpText('after', `
8542
8543
  Examples:
@@ -8545,6 +8546,7 @@ Examples:
8545
8546
  fig read "Login Screen" --tokens Show only the design tokens used by Login Screen
8546
8547
  fig read --selection Read the node you currently have selected in Figma
8547
8548
  fig read --link "https://..." Read a node from a Figma selection link
8549
+ fig read --page Read the entire page with all frames, structure, and tokens
8548
8550
  fig read --json Output raw JSON (useful for piping to other tools)
8549
8551
  `)
8550
8552
  .action(async (frameName, options) => {
@@ -8560,14 +8562,14 @@ Examples:
8560
8562
  if (!sel || sel.length === 0) return { error: 'Nothing selected in Figma. Select a frame or layer first.' };
8561
8563
  const node = sel[0];
8562
8564
  function walk(n, depth) {
8563
- if (depth > 4) return { type: n.type, name: n.name, truncated: true };
8565
+ if (depth > 20) return { type: n.type, name: n.name };
8564
8566
  const obj = { type: n.type, name: n.name };
8565
8567
  if (n.width) obj.w = Math.round(n.width);
8566
8568
  if (n.height) obj.h = Math.round(n.height);
8567
- if (n.type === 'TEXT') obj.text = n.characters.slice(0, 100);
8569
+ if (n.type === 'TEXT') obj.text = n.characters;
8568
8570
  if (n.fills && n.fills.length) obj.fills = n.fills.map(f => f.type === 'SOLID' ? { r: Math.round(f.color.r*255), g: Math.round(f.color.g*255), b: Math.round(f.color.b*255) } : { type: f.type });
8569
8571
  if (n.cornerRadius) obj.radius = n.cornerRadius;
8570
- if (n.children) obj.children = n.children.slice(0, 20).map(c => walk(c, depth + 1));
8572
+ if (n.children) obj.children = n.children.map(c => walk(c, depth + 1));
8571
8573
  return obj;
8572
8574
  }
8573
8575
  return { selected: sel.length, node: walk(node, 0) };
@@ -8598,14 +8600,14 @@ Examples:
8598
8600
  const node = figma.getNodeById('${nodeId}');
8599
8601
  if (!node) return { error: 'Node ${nodeId} not found on the current page.' };
8600
8602
  function walk(n, depth) {
8601
- if (depth > 4) return { type: n.type, name: n.name, truncated: true };
8603
+ if (depth > 20) return { type: n.type, name: n.name };
8602
8604
  const obj = { type: n.type, name: n.name };
8603
8605
  if (n.width) obj.w = Math.round(n.width);
8604
8606
  if (n.height) obj.h = Math.round(n.height);
8605
- if (n.type === 'TEXT') obj.text = n.characters.slice(0, 100);
8607
+ if (n.type === 'TEXT') obj.text = n.characters;
8606
8608
  if (n.fills && n.fills.length) obj.fills = n.fills.map(f => f.type === 'SOLID' ? { r: Math.round(f.color.r*255), g: Math.round(f.color.g*255), b: Math.round(f.color.b*255) } : { type: f.type });
8607
8609
  if (n.cornerRadius) obj.radius = n.cornerRadius;
8608
- if (n.children) obj.children = n.children.slice(0, 20).map(c => walk(c, depth + 1));
8610
+ if (n.children) obj.children = n.children.map(c => walk(c, depth + 1));
8609
8611
  return obj;
8610
8612
  }
8611
8613
  return { nodeId: '${nodeId}', node: walk(node, 0) };
@@ -8620,6 +8622,136 @@ Examples:
8620
8622
  return;
8621
8623
  }
8622
8624
 
8625
+ // --page: read the entire current page with all frames
8626
+ if (options.page) {
8627
+ spinner.text = 'Reading entire page...';
8628
+ const pageCode = `(function() {
8629
+ const page = figma.currentPage;
8630
+ function walk(n, depth) {
8631
+ if (depth > 20) return { type: n.type, name: n.name };
8632
+ var obj = { type: n.type, name: n.name, id: n.id };
8633
+ if (n.width) obj.w = Math.round(n.width);
8634
+ if (n.height) obj.h = Math.round(n.height);
8635
+ if (n.x !== undefined) obj.x = Math.round(n.x);
8636
+ if (n.y !== undefined) obj.y = Math.round(n.y);
8637
+ if (n.type === 'TEXT') obj.text = n.characters;
8638
+ if (n.fills && n.fills.length) {
8639
+ obj.fills = n.fills.map(function(f) {
8640
+ if (f.type === 'SOLID') return { type: 'SOLID', r: Math.round(f.color.r*255), g: Math.round(f.color.g*255), b: Math.round(f.color.b*255), a: f.opacity !== undefined ? f.opacity : 1 };
8641
+ return { type: f.type };
8642
+ });
8643
+ }
8644
+ if (n.strokes && n.strokes.length) {
8645
+ obj.strokes = n.strokes.map(function(s) {
8646
+ if (s.type === 'SOLID') return { type: 'SOLID', r: Math.round(s.color.r*255), g: Math.round(s.color.g*255), b: Math.round(s.color.b*255) };
8647
+ return { type: s.type };
8648
+ });
8649
+ if (n.strokeWeight) obj.strokeWeight = n.strokeWeight;
8650
+ }
8651
+ if (n.cornerRadius) obj.radius = n.cornerRadius;
8652
+ if (n.opacity !== undefined && n.opacity < 1) obj.opacity = n.opacity;
8653
+ if (n.layoutMode) {
8654
+ obj.layout = { mode: n.layoutMode };
8655
+ if (n.itemSpacing) obj.layout.gap = n.itemSpacing;
8656
+ if (n.paddingTop || n.paddingRight || n.paddingBottom || n.paddingLeft) {
8657
+ obj.layout.padding = { top: n.paddingTop || 0, right: n.paddingRight || 0, bottom: n.paddingBottom || 0, left: n.paddingLeft || 0 };
8658
+ }
8659
+ if (n.primaryAxisAlignItems) obj.layout.mainAlign = n.primaryAxisAlignItems;
8660
+ if (n.counterAxisAlignItems) obj.layout.crossAlign = n.counterAxisAlignItems;
8661
+ }
8662
+ if (n.type === 'TEXT') {
8663
+ obj.typography = {};
8664
+ try { obj.typography.family = n.fontName.family; } catch(e) {}
8665
+ try { obj.typography.style = n.fontName.style; } catch(e) {}
8666
+ try { obj.typography.size = n.fontSize; } catch(e) {}
8667
+ try { obj.typography.weight = n.fontWeight; } catch(e) {}
8668
+ try {
8669
+ if (n.lineHeight && n.lineHeight.value) obj.typography.lineHeight = n.lineHeight.unit === 'PERCENT' ? n.lineHeight.value + '%' : n.lineHeight.value;
8670
+ } catch(e) {}
8671
+ try { if (n.letterSpacing && n.letterSpacing.value) obj.typography.letterSpacing = n.letterSpacing.value; } catch(e) {}
8672
+ }
8673
+ if (n.effects && n.effects.length) {
8674
+ obj.effects = n.effects.map(function(e) {
8675
+ var eff = { type: e.type };
8676
+ if (e.radius) eff.radius = e.radius;
8677
+ if (e.offset) eff.offset = { x: e.offset.x, y: e.offset.y };
8678
+ if (e.color) eff.color = { r: Math.round(e.color.r*255), g: Math.round(e.color.g*255), b: Math.round(e.color.b*255), a: e.color.a };
8679
+ return eff;
8680
+ });
8681
+ }
8682
+ // Variable bindings
8683
+ try {
8684
+ var vars = {};
8685
+ var bindings = n.boundVariables;
8686
+ if (bindings) {
8687
+ Object.keys(bindings).forEach(function(prop) {
8688
+ try {
8689
+ var binding = bindings[prop];
8690
+ if (Array.isArray(binding)) binding = binding[0];
8691
+ if (binding && binding.id) {
8692
+ var v = figma.variables.getVariableById(binding.id);
8693
+ if (v) {
8694
+ var info = { name: v.name };
8695
+ try {
8696
+ var collection = figma.variables.getVariableCollectionById(v.variableCollectionId);
8697
+ if (collection && collection.modes && collection.modes.length > 0) {
8698
+ info.values = {};
8699
+ collection.modes.forEach(function(mode) {
8700
+ try {
8701
+ var mVal = v.valuesByMode[mode.modeId];
8702
+ if (mVal && mVal.type === 'VARIABLE_ALIAS') {
8703
+ try {
8704
+ var aliased = figma.variables.getVariableById(mVal.id);
8705
+ info.values[mode.name] = aliased ? aliased.name : 'alias';
8706
+ } catch(e3) { info.values[mode.name] = 'alias'; }
8707
+ } else {
8708
+ info.values[mode.name] = mVal;
8709
+ }
8710
+ } catch(e4) {}
8711
+ });
8712
+ }
8713
+ } catch(e5) {}
8714
+ vars[prop] = info;
8715
+ }
8716
+ }
8717
+ } catch(e) {}
8718
+ });
8719
+ }
8720
+ if (Object.keys(vars).length > 0) obj.variables = vars;
8721
+ } catch(e) {}
8722
+ // Component info
8723
+ if (n.type === 'COMPONENT') obj.isComponent = true;
8724
+ if (n.type === 'INSTANCE') {
8725
+ obj.isInstance = true;
8726
+ try { obj.componentName = n.mainComponent ? n.mainComponent.name : undefined; } catch(e) {}
8727
+ }
8728
+ if (n.children) obj.children = n.children.map(function(c) { return walk(c, depth + 1); });
8729
+ return obj;
8730
+ }
8731
+ var frames = [];
8732
+ for (var i = 0; i < page.children.length; i++) {
8733
+ frames.push(walk(page.children[i], 0));
8734
+ }
8735
+ return { page: page.name, frameCount: frames.length, frames: frames };
8736
+ })()`;
8737
+ const result = await daemonExec('eval', { code: pageCode });
8738
+ if (result.error) {
8739
+ spinner.fail(result.error);
8740
+ process.exit(1);
8741
+ }
8742
+ spinner.succeed(`Read page "${result.page}" — ${result.frameCount} top-level frames`);
8743
+ if (options.json) {
8744
+ console.log(JSON.stringify(result, null, 2));
8745
+ } else {
8746
+ // Pretty print each frame
8747
+ for (const frame of result.frames) {
8748
+ console.log(chalk.cyan(`\n━━━ ${frame.name} [${frame.type}] ${frame.w || ''}x${frame.h || ''} ━━━`));
8749
+ console.log(formatSelectionResult({ node: frame }));
8750
+ }
8751
+ }
8752
+ return;
8753
+ }
8754
+
8623
8755
  // Stage 1: metadata — always run, cheapest call
8624
8756
  spinner.text = 'Scanning canvas...';
8625
8757
  const metadata = await daemonExec('eval', { code: STAGE1_METADATA });
@@ -8999,7 +9131,7 @@ const INSPECT_CODE = `(function() {
8999
9131
  // Children summary
9000
9132
  if (node.children && node.children.length > 0) {
9001
9133
  spec.childCount = node.children.length;
9002
- spec.children = node.children.slice(0, 30).map(function(c) {
9134
+ spec.children = node.children.map(function(c) {
9003
9135
  return { name: c.name, type: c.type, w: c.width ? Math.round(c.width) : undefined, h: c.height ? Math.round(c.height) : undefined };
9004
9136
  });
9005
9137
  }
@@ -9007,9 +9139,9 @@ const INSPECT_CODE = `(function() {
9007
9139
  return spec;
9008
9140
  }
9009
9141
 
9010
- // Inspect all selected nodes (up to 10)
9142
+ // Inspect all selected nodes
9011
9143
  var results = [];
9012
- var count = Math.min(sel.length, 10);
9144
+ var count = sel.length;
9013
9145
  for (var i = 0; i < count; i++) {
9014
9146
  results.push(inspectNode(sel[i]));
9015
9147
  }
@@ -9083,7 +9215,7 @@ Examples:
9083
9215
  ${options.node ? `node = figma.getNodeById('${options.node}');` : ''}
9084
9216
  ${options.link ? `node = figma.getNodeById('${parseNodeIdFromLink(options.link)}');` : ''}
9085
9217
  if (!node || !node.children) return [];
9086
- return node.children.slice(0, 30).map(function(c) { return c.id; });
9218
+ return node.children.map(function(c) { return c.id; });
9087
9219
  })()`;
9088
9220
  const childIds = await daemonExec('eval', { code: childIdsCode });
9089
9221
  if (Array.isArray(childIds)) {
@@ -9199,7 +9331,7 @@ function formatInspectSpec(spec) {
9199
9331
  if (t.textAlign) lines.push(` Text align: ${t.textAlign}`);
9200
9332
  if (t.textDecoration) lines.push(` Decoration: ${t.textDecoration}`);
9201
9333
  if (t.textTransform) lines.push(` Transform: ${t.textTransform}`);
9202
- if (t.content) lines.push(` Content: "${t.content.slice(0, 80)}${t.content.length > 80 ? '...' : ''}"`);
9334
+ if (t.content) lines.push(` Content: "${t.content}"`);
9203
9335
  }
9204
9336
 
9205
9337
  // Effects
@@ -9530,7 +9662,7 @@ function formatCSS(node) {
9530
9662
  }
9531
9663
  lines.push('}');
9532
9664
  if (node.text) {
9533
- lines.push(chalk.gray(`/* Content: "${node.text.slice(0, 60)}${node.text.length > 60 ? '...' : ''}" */`));
9665
+ lines.push(chalk.gray(`/* Content: "${node.text}" */`));
9534
9666
  }
9535
9667
  return lines.join('\n');
9536
9668
  }
@@ -9610,7 +9742,7 @@ function formatTailwind(node) {
9610
9742
  const lines = [];
9611
9743
  lines.push(chalk.gray(`{/* ${node.name} */}`));
9612
9744
  lines.push(`className="${classes.join(' ')}"`);
9613
- if (node.text) lines.push(chalk.gray(`{/* "${node.text.slice(0, 60)}${node.text.length > 60 ? '...' : ''}" */}`));
9745
+ if (node.text) lines.push(chalk.gray(`{/* "${node.text}" */}`));
9614
9746
  return lines.join('\n');
9615
9747
  }
9616
9748
 
@@ -9889,7 +10021,7 @@ const DOCUMENT_CODE = `(function() {
9889
10021
  }
9890
10022
 
9891
10023
  function extractNode(node, depth) {
9892
- if (depth > 15) return null;
10024
+ if (depth > 30) return null;
9893
10025
  var n = { name: node.name, type: node.type };
9894
10026
 
9895
10027
  // Dimensions