figma-local 1.5.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 +132 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-local",
3
- "version": "1.5.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) => {
@@ -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 });