figma-local 1.0.0 → 1.2.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/README.md +95 -18
- package/bin/fig-start +28 -12
- package/package.json +8 -6
- package/plugin/code.js +178 -0
- package/plugin/manifest.json +14 -0
- package/plugin/ui.html +285 -0
- package/skills/figma-css/SKILL.md +119 -0
- package/skills/figma-document/SKILL.md +129 -0
- package/skills/figma-inspect/SKILL.md +98 -0
- package/skills/figma-local/SKILL.md +170 -0
- package/skills/figma-measure/SKILL.md +59 -0
- package/skills/figma-styles/SKILL.md +114 -0
- package/src/daemon.js +1 -1
- package/src/index.js +1772 -25
package/src/index.js
CHANGED
|
@@ -1039,7 +1039,7 @@ program
|
|
|
1039
1039
|
|
|
1040
1040
|
// Show plugin setup instructions
|
|
1041
1041
|
console.log(chalk.hex('#FF6B35')('\n ┌─────────────────────────────────────────────────────┐'));
|
|
1042
|
-
console.log(chalk.hex('#FF6B35')(' │') + chalk.white.bold(' Setup the
|
|
1042
|
+
console.log(chalk.hex('#FF6B35')(' │') + chalk.white.bold(' Setup the Figma Local plugin ') + chalk.hex('#FF6B35')('│'));
|
|
1043
1043
|
console.log(chalk.hex('#FF6B35')(' └─────────────────────────────────────────────────────┘\n'));
|
|
1044
1044
|
|
|
1045
1045
|
console.log(chalk.white.bold(' ONE-TIME SETUP:\n'));
|
|
@@ -1049,7 +1049,7 @@ program
|
|
|
1049
1049
|
console.log(chalk.cyan(' 4. ') + chalk.white('Click ') + chalk.yellow('Open') + chalk.white(' — plugin is now installed!\n'));
|
|
1050
1050
|
|
|
1051
1051
|
console.log(chalk.white.bold(' EACH SESSION:\n'));
|
|
1052
|
-
console.log(chalk.cyan(' → ') + chalk.white('In Figma: ') + chalk.yellow('Plugins → Development →
|
|
1052
|
+
console.log(chalk.cyan(' → ') + chalk.white('In Figma: ') + chalk.yellow('Plugins → Development → Figma Local\n'));
|
|
1053
1053
|
|
|
1054
1054
|
console.log(chalk.gray(' 💡 Tip: Right-click plugin → "Add to toolbar" for one-click access\n'));
|
|
1055
1055
|
|
|
@@ -6764,7 +6764,31 @@ node
|
|
|
6764
6764
|
const b = Array.isArray(binding) ? binding[0] : binding;
|
|
6765
6765
|
if (b && b.id) {
|
|
6766
6766
|
const variable = figma.variables.getVariableById(b.id);
|
|
6767
|
-
|
|
6767
|
+
if (variable) {
|
|
6768
|
+
const info = { name: variable.name };
|
|
6769
|
+
try {
|
|
6770
|
+
const modeIds = Object.keys(variable.valuesByMode);
|
|
6771
|
+
if (modeIds.length > 0) {
|
|
6772
|
+
const val = variable.valuesByMode[modeIds[0]];
|
|
6773
|
+
if (val !== undefined) {
|
|
6774
|
+
if (typeof val === 'object' && val.r !== undefined) {
|
|
6775
|
+
const r = Math.round((val.r||0)*255), g = Math.round((val.g||0)*255), bl = Math.round((val.b||0)*255);
|
|
6776
|
+
info.value = '#' + r.toString(16).padStart(2,'0') + g.toString(16).padStart(2,'0') + bl.toString(16).padStart(2,'0');
|
|
6777
|
+
} else if (typeof val === 'object' && val.id) {
|
|
6778
|
+
try {
|
|
6779
|
+
const alias = figma.variables.getVariableById(val.id);
|
|
6780
|
+
info.value = alias ? alias.name + ' (alias)' : 'alias';
|
|
6781
|
+
} catch(e) { info.value = 'alias'; }
|
|
6782
|
+
} else {
|
|
6783
|
+
info.value = val;
|
|
6784
|
+
}
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6787
|
+
} catch(e) {}
|
|
6788
|
+
bindings[prop] = info;
|
|
6789
|
+
} else {
|
|
6790
|
+
bindings[prop] = { name: b.id };
|
|
6791
|
+
}
|
|
6768
6792
|
}
|
|
6769
6793
|
}
|
|
6770
6794
|
}
|
|
@@ -8354,21 +8378,100 @@ blocksCmd
|
|
|
8354
8378
|
|
|
8355
8379
|
program
|
|
8356
8380
|
.command('read [frameName]')
|
|
8357
|
-
.description('
|
|
8358
|
-
.option('--
|
|
8359
|
-
.option('--
|
|
8360
|
-
.option('--
|
|
8361
|
-
.option('--
|
|
8381
|
+
.description('Read your Figma canvas — list all frames, or dive into one for layout, components, and tokens')
|
|
8382
|
+
.option('--json', 'Output raw JSON instead of formatted text')
|
|
8383
|
+
.option('--tokens', 'Show only the design tokens (colors, spacing, etc.) used in the frame')
|
|
8384
|
+
.option('--selection', 'Read whatever is currently selected in Figma')
|
|
8385
|
+
.option('--link <url>', 'Read a specific node from a Figma selection link (e.g. copied via "Copy link to selection")')
|
|
8386
|
+
.option('--stage <n>', 'Run a specific stage: 1 (list frames), 2 (structure), 3 (tokens)')
|
|
8387
|
+
.addHelpText('after', `
|
|
8388
|
+
Examples:
|
|
8389
|
+
fig read List all frames on the current page
|
|
8390
|
+
fig read "Login Screen" Get layout, components, and tokens for the Login Screen frame
|
|
8391
|
+
fig read "Login Screen" --tokens Show only the design tokens used by Login Screen
|
|
8392
|
+
fig read --selection Read the node you currently have selected in Figma
|
|
8393
|
+
fig read --link "https://..." Read a node from a Figma selection link
|
|
8394
|
+
fig read --json Output raw JSON (useful for piping to other tools)
|
|
8395
|
+
`)
|
|
8362
8396
|
.action(async (frameName, options) => {
|
|
8363
8397
|
checkConnection();
|
|
8364
|
-
const spinner = ora('Reading design
|
|
8398
|
+
const spinner = ora('Reading design...').start();
|
|
8365
8399
|
|
|
8366
8400
|
try {
|
|
8401
|
+
// --selection: read the currently selected node in Figma
|
|
8402
|
+
if (options.selection) {
|
|
8403
|
+
spinner.text = 'Reading current selection...';
|
|
8404
|
+
const selectionCode = `(function() {
|
|
8405
|
+
const sel = figma.currentPage.selection;
|
|
8406
|
+
if (!sel || sel.length === 0) return { error: 'Nothing selected in Figma. Select a frame or layer first.' };
|
|
8407
|
+
const node = sel[0];
|
|
8408
|
+
function walk(n, depth) {
|
|
8409
|
+
if (depth > 4) return { type: n.type, name: n.name, truncated: true };
|
|
8410
|
+
const obj = { type: n.type, name: n.name };
|
|
8411
|
+
if (n.width) obj.w = Math.round(n.width);
|
|
8412
|
+
if (n.height) obj.h = Math.round(n.height);
|
|
8413
|
+
if (n.type === 'TEXT') obj.text = n.characters.slice(0, 100);
|
|
8414
|
+
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 });
|
|
8415
|
+
if (n.cornerRadius) obj.radius = n.cornerRadius;
|
|
8416
|
+
if (n.children) obj.children = n.children.slice(0, 20).map(c => walk(c, depth + 1));
|
|
8417
|
+
return obj;
|
|
8418
|
+
}
|
|
8419
|
+
return { selected: sel.length, node: walk(node, 0) };
|
|
8420
|
+
})()`;
|
|
8421
|
+
const result = await daemonExec('eval', { code: selectionCode });
|
|
8422
|
+
if (result.error) {
|
|
8423
|
+
spinner.fail(result.error);
|
|
8424
|
+
process.exit(1);
|
|
8425
|
+
}
|
|
8426
|
+
spinner.succeed(`Reading selection: ${result.node.name}`);
|
|
8427
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatSelectionResult(result));
|
|
8428
|
+
return;
|
|
8429
|
+
}
|
|
8430
|
+
|
|
8431
|
+
// --link: parse Figma URL and read a specific node by ID
|
|
8432
|
+
if (options.link) {
|
|
8433
|
+
spinner.text = 'Reading node from link...';
|
|
8434
|
+
// Figma links contain node-id as a query param like ?node-id=123:456 or ?node-id=123-456
|
|
8435
|
+
const url = options.link;
|
|
8436
|
+
const nodeIdMatch = url.match(/node-id=([0-9]+-[0-9]+|[0-9]+:[0-9]+|[0-9]+%3A[0-9]+)/i);
|
|
8437
|
+
if (!nodeIdMatch) {
|
|
8438
|
+
spinner.fail('Could not find a node ID in that link. Copy the link from Figma via right-click → "Copy/Paste as" → "Copy link to selection".');
|
|
8439
|
+
process.exit(1);
|
|
8440
|
+
}
|
|
8441
|
+
// Normalize: Figma URLs use - or %3A, but the API expects :
|
|
8442
|
+
const nodeId = decodeURIComponent(nodeIdMatch[1]).replace('-', ':');
|
|
8443
|
+
const nodeCode = `(function() {
|
|
8444
|
+
const node = figma.getNodeById('${nodeId}');
|
|
8445
|
+
if (!node) return { error: 'Node ${nodeId} not found on the current page.' };
|
|
8446
|
+
function walk(n, depth) {
|
|
8447
|
+
if (depth > 4) return { type: n.type, name: n.name, truncated: true };
|
|
8448
|
+
const obj = { type: n.type, name: n.name };
|
|
8449
|
+
if (n.width) obj.w = Math.round(n.width);
|
|
8450
|
+
if (n.height) obj.h = Math.round(n.height);
|
|
8451
|
+
if (n.type === 'TEXT') obj.text = n.characters.slice(0, 100);
|
|
8452
|
+
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 });
|
|
8453
|
+
if (n.cornerRadius) obj.radius = n.cornerRadius;
|
|
8454
|
+
if (n.children) obj.children = n.children.slice(0, 20).map(c => walk(c, depth + 1));
|
|
8455
|
+
return obj;
|
|
8456
|
+
}
|
|
8457
|
+
return { nodeId: '${nodeId}', node: walk(node, 0) };
|
|
8458
|
+
})()`;
|
|
8459
|
+
const result = await daemonExec('eval', { code: nodeCode });
|
|
8460
|
+
if (result.error) {
|
|
8461
|
+
spinner.fail(result.error);
|
|
8462
|
+
process.exit(1);
|
|
8463
|
+
}
|
|
8464
|
+
spinner.succeed(`Reading node: ${result.node.name} (${nodeId})`);
|
|
8465
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatSelectionResult(result));
|
|
8466
|
+
return;
|
|
8467
|
+
}
|
|
8468
|
+
|
|
8367
8469
|
// Stage 1: metadata — always run, cheapest call
|
|
8470
|
+
spinner.text = 'Scanning canvas...';
|
|
8368
8471
|
const metadata = await daemonExec('eval', { code: STAGE1_METADATA });
|
|
8369
8472
|
|
|
8370
8473
|
if (options.stage === '1') {
|
|
8371
|
-
spinner.succeed('
|
|
8474
|
+
spinner.succeed('Canvas scanned');
|
|
8372
8475
|
console.log(options.json ? JSON.stringify(metadata, null, 2) : formatStage1(metadata));
|
|
8373
8476
|
return;
|
|
8374
8477
|
}
|
|
@@ -8387,44 +8490,41 @@ program
|
|
|
8387
8490
|
} else if (metadata.frames.length === 1) {
|
|
8388
8491
|
targetFrame = metadata.frames[0];
|
|
8389
8492
|
} else {
|
|
8390
|
-
spinner.succeed(
|
|
8391
|
-
console.log('
|
|
8392
|
-
metadata.frames.forEach(f => console.log(` • ${f.name} ${f.w}x${f.h}`));
|
|
8393
|
-
console.log(
|
|
8493
|
+
spinner.succeed(`Found ${metadata.frames.length} frames on "${metadata.page}"`);
|
|
8494
|
+
console.log('');
|
|
8495
|
+
metadata.frames.forEach(f => console.log(` • ${f.name} ${f.w}x${f.h} [${f.type}]`));
|
|
8496
|
+
console.log(`\nTo read a specific frame:\n fig read "${metadata.frames[0].name}"`);
|
|
8394
8497
|
return;
|
|
8395
8498
|
}
|
|
8396
8499
|
|
|
8397
|
-
// Stage 2: frame structure
|
|
8398
|
-
spinner.text = `
|
|
8500
|
+
// Stage 2: frame structure
|
|
8501
|
+
spinner.text = `Reading "${targetFrame.name}"...`;
|
|
8399
8502
|
const frameStructure = await daemonExec('eval', { code: buildFrameStructureCode(targetFrame.id) });
|
|
8400
8503
|
|
|
8401
8504
|
if (options.stage === '2') {
|
|
8402
|
-
spinner.succeed('
|
|
8505
|
+
spinner.succeed('Structure extracted');
|
|
8403
8506
|
console.log(options.json ? JSON.stringify(frameStructure, null, 2) : JSON.stringify(frameStructure, null, 2));
|
|
8404
8507
|
return;
|
|
8405
8508
|
}
|
|
8406
8509
|
|
|
8407
8510
|
if (options.tokens) {
|
|
8408
|
-
|
|
8409
|
-
spinner.text = 'Stage 3: extracting used tokens only...';
|
|
8511
|
+
spinner.text = 'Extracting design tokens...';
|
|
8410
8512
|
const tokens = await daemonExec('eval', { code: buildUsedTokensCode(targetFrame.id) });
|
|
8411
|
-
spinner.succeed(
|
|
8513
|
+
spinner.succeed(`${Object.keys(tokens).length} tokens used in "${targetFrame.name}"`);
|
|
8412
8514
|
console.log(options.json ? JSON.stringify(tokens, null, 2) : formatTokensOnly(tokens));
|
|
8413
8515
|
return;
|
|
8414
8516
|
}
|
|
8415
8517
|
|
|
8416
|
-
// Stage 3: used tokens
|
|
8417
|
-
spinner.text = '
|
|
8518
|
+
// Stage 3: used tokens
|
|
8519
|
+
spinner.text = 'Extracting tokens...';
|
|
8418
8520
|
const tokens = await daemonExec('eval', { code: buildUsedTokensCode(targetFrame.id) });
|
|
8419
8521
|
|
|
8420
8522
|
spinner.succeed(`Read complete — ${targetFrame.name}`);
|
|
8421
8523
|
console.log('');
|
|
8422
8524
|
|
|
8423
|
-
// Format and output
|
|
8424
8525
|
if (options.json) {
|
|
8425
8526
|
console.log(JSON.stringify({ metadata, frame: frameStructure, tokens }, null, 2));
|
|
8426
8527
|
} else {
|
|
8427
|
-
// Default: lean text block
|
|
8428
8528
|
const ctx = formatLeanContext(metadata, frameStructure, tokens, targetFrame.name);
|
|
8429
8529
|
console.log(ctx);
|
|
8430
8530
|
}
|
|
@@ -8447,11 +8547,1658 @@ function formatTokensOnly(tokens) {
|
|
|
8447
8547
|
keys.map(k => ` ${k}: ${tokens[k]}`).join('\n');
|
|
8448
8548
|
}
|
|
8449
8549
|
|
|
8550
|
+
function formatSelectionResult(result) {
|
|
8551
|
+
const lines = [];
|
|
8552
|
+
function printNode(node, depth) {
|
|
8553
|
+
if (!node) return;
|
|
8554
|
+
const indent = ' '.repeat(depth);
|
|
8555
|
+
let line = `${indent}[${node.type}] ${node.name}`;
|
|
8556
|
+
if (node.w && node.h) line += ` ${node.w}x${node.h}`;
|
|
8557
|
+
if (node.text) line += ` "${node.text}"`;
|
|
8558
|
+
if (node.radius) line += ` radius=${node.radius}`;
|
|
8559
|
+
if (node.fills && node.fills.length) {
|
|
8560
|
+
const colors = node.fills.map(f => f.r !== undefined ? `rgb(${f.r},${f.g},${f.b})` : f.type).join(', ');
|
|
8561
|
+
line += ` fill=${colors}`;
|
|
8562
|
+
}
|
|
8563
|
+
if (node.truncated) line += ' ...';
|
|
8564
|
+
lines.push(line);
|
|
8565
|
+
if (node.children) node.children.forEach(c => printNode(c, depth + 1));
|
|
8566
|
+
}
|
|
8567
|
+
printNode(result.node, 0);
|
|
8568
|
+
return lines.join('\n');
|
|
8569
|
+
}
|
|
8570
|
+
|
|
8571
|
+
// ============ INSPECT — detailed design specs ============
|
|
8572
|
+
|
|
8573
|
+
const INSPECT_CODE = `(function() {
|
|
8574
|
+
var sel = figma.currentPage.selection;
|
|
8575
|
+
if (!sel || sel.length === 0) return { error: 'Nothing selected. Select a frame, component, or layer in Figma first.' };
|
|
8576
|
+
|
|
8577
|
+
var BASE = 16; // 1rem = 16px
|
|
8578
|
+
|
|
8579
|
+
function toRem(px) {
|
|
8580
|
+
return parseFloat((px / BASE).toFixed(3));
|
|
8581
|
+
}
|
|
8582
|
+
|
|
8583
|
+
function colorToHex(c) {
|
|
8584
|
+
var r = Math.round((c.r || 0) * 255);
|
|
8585
|
+
var g = Math.round((c.g || 0) * 255);
|
|
8586
|
+
var b = Math.round((c.b || 0) * 255);
|
|
8587
|
+
return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0');
|
|
8588
|
+
}
|
|
8589
|
+
|
|
8590
|
+
function colorToRgba(c, opacity) {
|
|
8591
|
+
var r = Math.round((c.r || 0) * 255);
|
|
8592
|
+
var g = Math.round((c.g || 0) * 255);
|
|
8593
|
+
var b = Math.round((c.b || 0) * 255);
|
|
8594
|
+
if (opacity !== undefined && opacity < 1) {
|
|
8595
|
+
return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + parseFloat(opacity.toFixed(2)) + ')';
|
|
8596
|
+
}
|
|
8597
|
+
return 'rgb(' + r + ', ' + g + ', ' + b + ')';
|
|
8598
|
+
}
|
|
8599
|
+
|
|
8600
|
+
function inspectNode(node) {
|
|
8601
|
+
var spec = {
|
|
8602
|
+
name: node.name,
|
|
8603
|
+
type: node.type,
|
|
8604
|
+
};
|
|
8605
|
+
|
|
8606
|
+
// Dimensions
|
|
8607
|
+
if (node.width !== undefined) {
|
|
8608
|
+
spec.dimensions = {
|
|
8609
|
+
width: { px: Math.round(node.width), rem: toRem(node.width) },
|
|
8610
|
+
height: { px: Math.round(node.height), rem: toRem(node.height) }
|
|
8611
|
+
};
|
|
8612
|
+
}
|
|
8613
|
+
|
|
8614
|
+
// Position
|
|
8615
|
+
if (node.x !== undefined) {
|
|
8616
|
+
spec.position = {
|
|
8617
|
+
x: { px: Math.round(node.x), rem: toRem(node.x) },
|
|
8618
|
+
y: { px: Math.round(node.y), rem: toRem(node.y) }
|
|
8619
|
+
};
|
|
8620
|
+
}
|
|
8621
|
+
|
|
8622
|
+
// Auto layout / Flexbox
|
|
8623
|
+
if (node.layoutMode && node.layoutMode !== 'NONE') {
|
|
8624
|
+
spec.layout = {
|
|
8625
|
+
mode: node.layoutMode === 'HORIZONTAL' ? 'row' : 'column',
|
|
8626
|
+
gap: { px: node.itemSpacing || 0, rem: toRem(node.itemSpacing || 0) },
|
|
8627
|
+
padding: {
|
|
8628
|
+
top: { px: node.paddingTop || 0, rem: toRem(node.paddingTop || 0) },
|
|
8629
|
+
right: { px: node.paddingRight || 0, rem: toRem(node.paddingRight || 0) },
|
|
8630
|
+
bottom: { px: node.paddingBottom || 0, rem: toRem(node.paddingBottom || 0) },
|
|
8631
|
+
left: { px: node.paddingLeft || 0, rem: toRem(node.paddingLeft || 0) }
|
|
8632
|
+
},
|
|
8633
|
+
mainAxisAlign: node.primaryAxisAlignItems || 'MIN',
|
|
8634
|
+
crossAxisAlign: node.counterAxisAlignItems || 'MIN',
|
|
8635
|
+
wrap: node.layoutWrap === 'WRAP' ? true : false
|
|
8636
|
+
};
|
|
8637
|
+
if (node.primaryAxisSizingMode) spec.layout.mainAxisSizing = node.primaryAxisSizingMode;
|
|
8638
|
+
if (node.counterAxisSizingMode) spec.layout.crossAxisSizing = node.counterAxisSizingMode;
|
|
8639
|
+
}
|
|
8640
|
+
|
|
8641
|
+
// Corner radius
|
|
8642
|
+
if (node.cornerRadius !== undefined && node.cornerRadius !== 0) {
|
|
8643
|
+
if (typeof node.cornerRadius === 'number') {
|
|
8644
|
+
spec.borderRadius = { px: node.cornerRadius, rem: toRem(node.cornerRadius) };
|
|
8645
|
+
}
|
|
8646
|
+
}
|
|
8647
|
+
// Per-corner radius
|
|
8648
|
+
if (node.topLeftRadius || node.topRightRadius || node.bottomRightRadius || node.bottomLeftRadius) {
|
|
8649
|
+
spec.borderRadiusPerCorner = {
|
|
8650
|
+
topLeft: { px: node.topLeftRadius || 0, rem: toRem(node.topLeftRadius || 0) },
|
|
8651
|
+
topRight: { px: node.topRightRadius || 0, rem: toRem(node.topRightRadius || 0) },
|
|
8652
|
+
bottomRight: { px: node.bottomRightRadius || 0, rem: toRem(node.bottomRightRadius || 0) },
|
|
8653
|
+
bottomLeft: { px: node.bottomLeftRadius || 0, rem: toRem(node.bottomLeftRadius || 0) }
|
|
8654
|
+
};
|
|
8655
|
+
}
|
|
8656
|
+
|
|
8657
|
+
// Fills
|
|
8658
|
+
if (node.fills && node.fills.length > 0) {
|
|
8659
|
+
spec.fills = [];
|
|
8660
|
+
for (var i = 0; i < node.fills.length; i++) {
|
|
8661
|
+
var f = node.fills[i];
|
|
8662
|
+
if (f.visible === false) continue;
|
|
8663
|
+
var fillSpec = { type: f.type };
|
|
8664
|
+
if (f.type === 'SOLID' && f.color) {
|
|
8665
|
+
fillSpec.hex = colorToHex(f.color);
|
|
8666
|
+
fillSpec.rgba = colorToRgba(f.color, f.opacity);
|
|
8667
|
+
fillSpec.opacity = f.opacity !== undefined ? f.opacity : 1;
|
|
8668
|
+
}
|
|
8669
|
+
if (f.type === 'GRADIENT_LINEAR' || f.type === 'GRADIENT_RADIAL') {
|
|
8670
|
+
fillSpec.stops = f.gradientStops ? f.gradientStops.map(function(s) {
|
|
8671
|
+
return { color: colorToHex(s.color), position: parseFloat(s.position.toFixed(2)) };
|
|
8672
|
+
}) : [];
|
|
8673
|
+
}
|
|
8674
|
+
spec.fills.push(fillSpec);
|
|
8675
|
+
}
|
|
8676
|
+
}
|
|
8677
|
+
|
|
8678
|
+
// Strokes
|
|
8679
|
+
if (node.strokes && node.strokes.length > 0) {
|
|
8680
|
+
spec.strokes = [];
|
|
8681
|
+
for (var i = 0; i < node.strokes.length; i++) {
|
|
8682
|
+
var s = node.strokes[i];
|
|
8683
|
+
if (s.visible === false) continue;
|
|
8684
|
+
var strokeSpec = { type: s.type };
|
|
8685
|
+
if (s.type === 'SOLID' && s.color) {
|
|
8686
|
+
strokeSpec.hex = colorToHex(s.color);
|
|
8687
|
+
strokeSpec.rgba = colorToRgba(s.color, s.opacity);
|
|
8688
|
+
}
|
|
8689
|
+
spec.strokes.push(strokeSpec);
|
|
8690
|
+
}
|
|
8691
|
+
if (node.strokeWeight) spec.strokeWeight = { px: node.strokeWeight, rem: toRem(node.strokeWeight) };
|
|
8692
|
+
if (node.strokeAlign) spec.strokeAlign = node.strokeAlign;
|
|
8693
|
+
}
|
|
8694
|
+
|
|
8695
|
+
// Typography
|
|
8696
|
+
if (node.type === 'TEXT') {
|
|
8697
|
+
spec.typography = {};
|
|
8698
|
+
spec.typography.content = node.characters;
|
|
8699
|
+
if (node.fontName && node.fontName !== figma.mixed) {
|
|
8700
|
+
spec.typography.fontFamily = node.fontName.family;
|
|
8701
|
+
spec.typography.fontStyle = node.fontName.style;
|
|
8702
|
+
}
|
|
8703
|
+
if (node.fontSize && node.fontSize !== figma.mixed) {
|
|
8704
|
+
spec.typography.fontSize = { px: node.fontSize, rem: toRem(node.fontSize) };
|
|
8705
|
+
}
|
|
8706
|
+
if (node.fontWeight && node.fontWeight !== figma.mixed) {
|
|
8707
|
+
spec.typography.fontWeight = node.fontWeight;
|
|
8708
|
+
}
|
|
8709
|
+
if (node.lineHeight && node.lineHeight !== figma.mixed) {
|
|
8710
|
+
if (node.lineHeight.unit === 'PIXELS') {
|
|
8711
|
+
spec.typography.lineHeight = { px: node.lineHeight.value, rem: toRem(node.lineHeight.value) };
|
|
8712
|
+
} else if (node.lineHeight.unit === 'PERCENT') {
|
|
8713
|
+
spec.typography.lineHeight = { percent: node.lineHeight.value + '%' };
|
|
8714
|
+
} else {
|
|
8715
|
+
spec.typography.lineHeight = 'auto';
|
|
8716
|
+
}
|
|
8717
|
+
}
|
|
8718
|
+
if (node.letterSpacing && node.letterSpacing !== figma.mixed) {
|
|
8719
|
+
if (node.letterSpacing.unit === 'PIXELS') {
|
|
8720
|
+
spec.typography.letterSpacing = { px: node.letterSpacing.value, rem: toRem(node.letterSpacing.value) };
|
|
8721
|
+
} else if (node.letterSpacing.unit === 'PERCENT') {
|
|
8722
|
+
spec.typography.letterSpacing = { percent: node.letterSpacing.value + '%' };
|
|
8723
|
+
}
|
|
8724
|
+
}
|
|
8725
|
+
if (node.textAlignHorizontal) spec.typography.textAlign = node.textAlignHorizontal.toLowerCase();
|
|
8726
|
+
if (node.textDecoration && node.textDecoration !== 'NONE' && node.textDecoration !== figma.mixed) {
|
|
8727
|
+
spec.typography.textDecoration = node.textDecoration.toLowerCase();
|
|
8728
|
+
}
|
|
8729
|
+
if (node.textCase && node.textCase !== 'ORIGINAL' && node.textCase !== figma.mixed) {
|
|
8730
|
+
spec.typography.textTransform = node.textCase.toLowerCase();
|
|
8731
|
+
}
|
|
8732
|
+
}
|
|
8733
|
+
|
|
8734
|
+
// Effects (shadows, blur)
|
|
8735
|
+
if (node.effects && node.effects.length > 0) {
|
|
8736
|
+
spec.effects = [];
|
|
8737
|
+
for (var i = 0; i < node.effects.length; i++) {
|
|
8738
|
+
var e = node.effects[i];
|
|
8739
|
+
if (e.visible === false) continue;
|
|
8740
|
+
var effectSpec = { type: e.type };
|
|
8741
|
+
if (e.type === 'DROP_SHADOW' || e.type === 'INNER_SHADOW') {
|
|
8742
|
+
effectSpec.offset = {
|
|
8743
|
+
x: { px: e.offset.x, rem: toRem(e.offset.x) },
|
|
8744
|
+
y: { px: e.offset.y, rem: toRem(e.offset.y) }
|
|
8745
|
+
};
|
|
8746
|
+
effectSpec.blur = { px: e.radius, rem: toRem(e.radius) };
|
|
8747
|
+
effectSpec.spread = { px: e.spread || 0, rem: toRem(e.spread || 0) };
|
|
8748
|
+
if (e.color) effectSpec.color = colorToRgba(e.color, e.color.a);
|
|
8749
|
+
}
|
|
8750
|
+
if (e.type === 'LAYER_BLUR' || e.type === 'BACKGROUND_BLUR') {
|
|
8751
|
+
effectSpec.blur = { px: e.radius, rem: toRem(e.radius) };
|
|
8752
|
+
}
|
|
8753
|
+
spec.effects.push(effectSpec);
|
|
8754
|
+
}
|
|
8755
|
+
}
|
|
8756
|
+
|
|
8757
|
+
// Opacity
|
|
8758
|
+
if (node.opacity !== undefined && node.opacity < 1) {
|
|
8759
|
+
spec.opacity = parseFloat(node.opacity.toFixed(2));
|
|
8760
|
+
}
|
|
8761
|
+
|
|
8762
|
+
// Constraints
|
|
8763
|
+
if (node.constraints) {
|
|
8764
|
+
spec.constraints = {
|
|
8765
|
+
horizontal: node.constraints.horizontal,
|
|
8766
|
+
vertical: node.constraints.vertical
|
|
8767
|
+
};
|
|
8768
|
+
}
|
|
8769
|
+
|
|
8770
|
+
// Component info
|
|
8771
|
+
if (node.type === 'INSTANCE' && node.mainComponent) {
|
|
8772
|
+
spec.component = {
|
|
8773
|
+
name: node.mainComponent.name,
|
|
8774
|
+
id: node.mainComponent.id
|
|
8775
|
+
};
|
|
8776
|
+
if (node.mainComponent.parent && node.mainComponent.parent.type === 'COMPONENT_SET') {
|
|
8777
|
+
spec.component.set = node.mainComponent.parent.name;
|
|
8778
|
+
}
|
|
8779
|
+
}
|
|
8780
|
+
if (node.type === 'COMPONENT') {
|
|
8781
|
+
spec.component = { name: node.name, id: node.id, isMainComponent: true };
|
|
8782
|
+
}
|
|
8783
|
+
|
|
8784
|
+
// Variable bindings
|
|
8785
|
+
if (node.boundVariables) {
|
|
8786
|
+
var vars = {};
|
|
8787
|
+
var bindings = node.boundVariables;
|
|
8788
|
+
Object.keys(bindings).forEach(function(prop) {
|
|
8789
|
+
var binding = bindings[prop];
|
|
8790
|
+
if (!binding) return;
|
|
8791
|
+
var entries = Array.isArray(binding) ? binding : [binding];
|
|
8792
|
+
entries.forEach(function(b) {
|
|
8793
|
+
if (!b || !b.id) return;
|
|
8794
|
+
try {
|
|
8795
|
+
var v = figma.variables.getVariableById(b.id);
|
|
8796
|
+
if (v) {
|
|
8797
|
+
var info = { name: v.name };
|
|
8798
|
+
try {
|
|
8799
|
+
var modeIds = Object.keys(v.valuesByMode);
|
|
8800
|
+
if (modeIds.length > 0) {
|
|
8801
|
+
var val = v.valuesByMode[modeIds[0]];
|
|
8802
|
+
if (val !== undefined) {
|
|
8803
|
+
if (typeof val === 'object' && val.r !== undefined) {
|
|
8804
|
+
info.value = colorToHex(val);
|
|
8805
|
+
} else if (typeof val === 'object' && val.id) {
|
|
8806
|
+
try {
|
|
8807
|
+
var alias = figma.variables.getVariableById(val.id);
|
|
8808
|
+
if (alias) info.value = alias.name + ' (alias)';
|
|
8809
|
+
} catch(e2) { info.value = 'alias'; }
|
|
8810
|
+
} else {
|
|
8811
|
+
info.value = val;
|
|
8812
|
+
}
|
|
8813
|
+
}
|
|
8814
|
+
}
|
|
8815
|
+
if (modeIds.length > 1) {
|
|
8816
|
+
info.modes = {};
|
|
8817
|
+
for (var mi = 0; mi < modeIds.length; mi++) {
|
|
8818
|
+
var mVal = v.valuesByMode[modeIds[mi]];
|
|
8819
|
+
try {
|
|
8820
|
+
var collection = figma.variables.getVariableCollectionById(v.variableCollectionId);
|
|
8821
|
+
var modeName = collection ? collection.modes.find(function(m) { return m.modeId === modeIds[mi]; }) : null;
|
|
8822
|
+
var mKey = modeName ? modeName.name : modeIds[mi];
|
|
8823
|
+
if (typeof mVal === 'object' && mVal.r !== undefined) {
|
|
8824
|
+
info.modes[mKey] = colorToHex(mVal);
|
|
8825
|
+
} else if (typeof mVal === 'object' && mVal.id) {
|
|
8826
|
+
try {
|
|
8827
|
+
var aliasM = figma.variables.getVariableById(mVal.id);
|
|
8828
|
+
info.modes[mKey] = aliasM ? aliasM.name : 'alias';
|
|
8829
|
+
} catch(e3) { info.modes[mKey] = 'alias'; }
|
|
8830
|
+
} else {
|
|
8831
|
+
info.modes[mKey] = mVal;
|
|
8832
|
+
}
|
|
8833
|
+
} catch(e4) {}
|
|
8834
|
+
}
|
|
8835
|
+
}
|
|
8836
|
+
} catch(e5) {}
|
|
8837
|
+
vars[prop] = info;
|
|
8838
|
+
}
|
|
8839
|
+
} catch(e) {}
|
|
8840
|
+
});
|
|
8841
|
+
});
|
|
8842
|
+
if (Object.keys(vars).length > 0) spec.variableBindings = vars;
|
|
8843
|
+
}
|
|
8844
|
+
|
|
8845
|
+
// Children summary
|
|
8846
|
+
if (node.children && node.children.length > 0) {
|
|
8847
|
+
spec.childCount = node.children.length;
|
|
8848
|
+
spec.children = node.children.slice(0, 30).map(function(c) {
|
|
8849
|
+
return { name: c.name, type: c.type, w: c.width ? Math.round(c.width) : undefined, h: c.height ? Math.round(c.height) : undefined };
|
|
8850
|
+
});
|
|
8851
|
+
}
|
|
8852
|
+
|
|
8853
|
+
return spec;
|
|
8854
|
+
}
|
|
8855
|
+
|
|
8856
|
+
// Inspect all selected nodes (up to 10)
|
|
8857
|
+
var results = [];
|
|
8858
|
+
var count = Math.min(sel.length, 10);
|
|
8859
|
+
for (var i = 0; i < count; i++) {
|
|
8860
|
+
results.push(inspectNode(sel[i]));
|
|
8861
|
+
}
|
|
8862
|
+
return { count: sel.length, specs: results };
|
|
8863
|
+
})()`;
|
|
8864
|
+
|
|
8865
|
+
function buildInspectCodeForNode(nodeId) {
|
|
8866
|
+
return INSPECT_CODE.replace(
|
|
8867
|
+
"var sel = figma.currentPage.selection;\n if (!sel || sel.length === 0) return { error: 'Nothing selected. Select a frame, component, or layer in Figma first.' };",
|
|
8868
|
+
`var targetNode = figma.getNodeById('${nodeId}');\n if (!targetNode) return { error: 'Node ${nodeId} not found.' };\n var sel = [targetNode];`
|
|
8869
|
+
);
|
|
8870
|
+
}
|
|
8871
|
+
|
|
8872
|
+
function parseNodeIdFromLink(url) {
|
|
8873
|
+
const match = url.match(/node-id=([0-9]+-[0-9]+|[0-9]+:[0-9]+|[0-9]+%3A[0-9]+)/i);
|
|
8874
|
+
if (!match) return null;
|
|
8875
|
+
return decodeURIComponent(match[1]).replace('-', ':');
|
|
8876
|
+
}
|
|
8877
|
+
|
|
8878
|
+
program
|
|
8879
|
+
.command('inspect')
|
|
8880
|
+
.description('Get detailed design specs for the selected element — spacing, colors, typography, effects (all in px and rem)')
|
|
8881
|
+
.option('--json', 'Output raw JSON')
|
|
8882
|
+
.option('--node <id>', 'Inspect a specific node by ID instead of selection')
|
|
8883
|
+
.option('--link <url>', 'Inspect a node from a Figma selection link')
|
|
8884
|
+
.option('--deep', 'Also inspect all children recursively (useful for components)')
|
|
8885
|
+
.addHelpText('after', `
|
|
8886
|
+
Examples:
|
|
8887
|
+
fig inspect Inspect what you have selected in Figma
|
|
8888
|
+
fig inspect --node "123:456" Inspect a specific node by ID
|
|
8889
|
+
fig inspect --link "https://..." Inspect from a Figma selection link
|
|
8890
|
+
fig inspect --deep Inspect selection + all children
|
|
8891
|
+
fig inspect --json Output raw JSON for programmatic use
|
|
8892
|
+
`)
|
|
8893
|
+
.action(async (options) => {
|
|
8894
|
+
checkConnection();
|
|
8895
|
+
const spinner = ora('Inspecting...').start();
|
|
8896
|
+
|
|
8897
|
+
try {
|
|
8898
|
+
let code = INSPECT_CODE;
|
|
8899
|
+
if (options.link) {
|
|
8900
|
+
const nodeId = parseNodeIdFromLink(options.link);
|
|
8901
|
+
if (!nodeId) {
|
|
8902
|
+
spinner.fail('Could not find a node ID in that link. Copy via right-click → "Copy link to selection" in Figma.');
|
|
8903
|
+
process.exit(1);
|
|
8904
|
+
}
|
|
8905
|
+
code = buildInspectCodeForNode(nodeId);
|
|
8906
|
+
} else if (options.node) {
|
|
8907
|
+
code = buildInspectCodeForNode(options.node);
|
|
8908
|
+
}
|
|
8909
|
+
|
|
8910
|
+
const result = await daemonExec('eval', { code });
|
|
8911
|
+
|
|
8912
|
+
if (result.error) {
|
|
8913
|
+
spinner.fail(result.error);
|
|
8914
|
+
process.exit(1);
|
|
8915
|
+
}
|
|
8916
|
+
|
|
8917
|
+
// If --deep, also inspect all children
|
|
8918
|
+
if (options.deep && result.specs.length > 0 && result.specs[0].children) {
|
|
8919
|
+
spinner.text = 'Inspecting children...';
|
|
8920
|
+
for (const child of result.specs[0].children) {
|
|
8921
|
+
// We need the child's node ID — get it from the parent
|
|
8922
|
+
// Use a separate eval to get child IDs
|
|
8923
|
+
}
|
|
8924
|
+
// Collect child node IDs from the parent
|
|
8925
|
+
const parentName = result.specs[0].name;
|
|
8926
|
+
const childIdsCode = `(function() {
|
|
8927
|
+
var sel = figma.currentPage.selection;
|
|
8928
|
+
var node = sel && sel.length > 0 ? sel[0] : null;
|
|
8929
|
+
${options.node ? `node = figma.getNodeById('${options.node}');` : ''}
|
|
8930
|
+
${options.link ? `node = figma.getNodeById('${parseNodeIdFromLink(options.link)}');` : ''}
|
|
8931
|
+
if (!node || !node.children) return [];
|
|
8932
|
+
return node.children.slice(0, 30).map(function(c) { return c.id; });
|
|
8933
|
+
})()`;
|
|
8934
|
+
const childIds = await daemonExec('eval', { code: childIdsCode });
|
|
8935
|
+
if (Array.isArray(childIds)) {
|
|
8936
|
+
for (const cid of childIds) {
|
|
8937
|
+
const childResult = await daemonExec('eval', { code: buildInspectCodeForNode(cid) });
|
|
8938
|
+
if (childResult && childResult.specs) {
|
|
8939
|
+
result.specs.push(...childResult.specs);
|
|
8940
|
+
}
|
|
8941
|
+
}
|
|
8942
|
+
}
|
|
8943
|
+
}
|
|
8944
|
+
|
|
8945
|
+
spinner.succeed(`Inspected ${result.specs.length} element${result.specs.length > 1 ? 's' : ''}`);
|
|
8946
|
+
|
|
8947
|
+
if (options.json) {
|
|
8948
|
+
console.log(JSON.stringify(result, null, 2));
|
|
8949
|
+
} else {
|
|
8950
|
+
console.log('');
|
|
8951
|
+
for (const spec of result.specs) {
|
|
8952
|
+
console.log(formatInspectSpec(spec));
|
|
8953
|
+
console.log('');
|
|
8954
|
+
}
|
|
8955
|
+
}
|
|
8956
|
+
} catch (e) {
|
|
8957
|
+
spinner.fail(`Inspect failed: ${e.message}`);
|
|
8958
|
+
process.exit(1);
|
|
8959
|
+
}
|
|
8960
|
+
});
|
|
8961
|
+
|
|
8962
|
+
function formatInspectSpec(spec) {
|
|
8963
|
+
const lines = [];
|
|
8964
|
+
const px = (v) => typeof v === 'object' && v.px !== undefined ? v.px : v;
|
|
8965
|
+
const rem = (v) => typeof v === 'object' && v.rem !== undefined ? v.rem : null;
|
|
8966
|
+
const dual = (v) => {
|
|
8967
|
+
if (!v || typeof v !== 'object') return String(v);
|
|
8968
|
+
if (v.px !== undefined && v.rem !== undefined) return `${v.px}px (${v.rem}rem)`;
|
|
8969
|
+
if (v.percent) return v.percent;
|
|
8970
|
+
return String(v);
|
|
8971
|
+
};
|
|
8972
|
+
|
|
8973
|
+
// Header
|
|
8974
|
+
lines.push(chalk.bold(`${spec.name}`) + chalk.gray(` [${spec.type}]`));
|
|
8975
|
+
lines.push(chalk.gray('─'.repeat(50)));
|
|
8976
|
+
|
|
8977
|
+
// Dimensions
|
|
8978
|
+
if (spec.dimensions) {
|
|
8979
|
+
lines.push(chalk.cyan('Dimensions'));
|
|
8980
|
+
lines.push(` Width: ${dual(spec.dimensions.width)}`);
|
|
8981
|
+
lines.push(` Height: ${dual(spec.dimensions.height)}`);
|
|
8982
|
+
}
|
|
8983
|
+
|
|
8984
|
+
// Position
|
|
8985
|
+
if (spec.position) {
|
|
8986
|
+
lines.push(chalk.cyan('Position'));
|
|
8987
|
+
lines.push(` X: ${dual(spec.position.x)} Y: ${dual(spec.position.y)}`);
|
|
8988
|
+
}
|
|
8989
|
+
|
|
8990
|
+
// Layout
|
|
8991
|
+
if (spec.layout) {
|
|
8992
|
+
lines.push(chalk.cyan('Layout (Auto Layout)'));
|
|
8993
|
+
lines.push(` Direction: ${spec.layout.mode}`);
|
|
8994
|
+
lines.push(` Gap: ${dual(spec.layout.gap)}`);
|
|
8995
|
+
const p = spec.layout.padding;
|
|
8996
|
+
lines.push(` Padding: ${px(p.top)}px ${px(p.right)}px ${px(p.bottom)}px ${px(p.left)}px (${rem(p.top)}rem ${rem(p.right)}rem ${rem(p.bottom)}rem ${rem(p.left)}rem)`);
|
|
8997
|
+
lines.push(` Align: main=${spec.layout.mainAxisAlign} cross=${spec.layout.crossAxisAlign}`);
|
|
8998
|
+
if (spec.layout.wrap) lines.push(` Wrap: yes`);
|
|
8999
|
+
}
|
|
9000
|
+
|
|
9001
|
+
// Border radius
|
|
9002
|
+
if (spec.borderRadius) {
|
|
9003
|
+
lines.push(chalk.cyan('Border Radius'));
|
|
9004
|
+
lines.push(` ${dual(spec.borderRadius)}`);
|
|
9005
|
+
}
|
|
9006
|
+
if (spec.borderRadiusPerCorner) {
|
|
9007
|
+
lines.push(chalk.cyan('Border Radius (per corner)'));
|
|
9008
|
+
const c = spec.borderRadiusPerCorner;
|
|
9009
|
+
lines.push(` TL: ${dual(c.topLeft)} TR: ${dual(c.topRight)} BR: ${dual(c.bottomRight)} BL: ${dual(c.bottomLeft)}`);
|
|
9010
|
+
}
|
|
9011
|
+
|
|
9012
|
+
// Fills
|
|
9013
|
+
if (spec.fills && spec.fills.length > 0) {
|
|
9014
|
+
lines.push(chalk.cyan('Fills'));
|
|
9015
|
+
for (const f of spec.fills) {
|
|
9016
|
+
if (f.type === 'SOLID') {
|
|
9017
|
+
lines.push(` ${f.hex} ${f.rgba}${f.opacity < 1 ? ` opacity=${f.opacity}` : ''}`);
|
|
9018
|
+
} else if (f.stops) {
|
|
9019
|
+
lines.push(` ${f.type}: ${f.stops.map(s => `${s.color} @${s.position}`).join(' → ')}`);
|
|
9020
|
+
} else {
|
|
9021
|
+
lines.push(` ${f.type}`);
|
|
9022
|
+
}
|
|
9023
|
+
}
|
|
9024
|
+
}
|
|
9025
|
+
|
|
9026
|
+
// Strokes
|
|
9027
|
+
if (spec.strokes && spec.strokes.length > 0) {
|
|
9028
|
+
lines.push(chalk.cyan('Strokes'));
|
|
9029
|
+
for (const s of spec.strokes) {
|
|
9030
|
+
let line = ` ${s.type === 'SOLID' ? s.hex : s.type}`;
|
|
9031
|
+
lines.push(line);
|
|
9032
|
+
}
|
|
9033
|
+
if (spec.strokeWeight) lines.push(` Weight: ${dual(spec.strokeWeight)} Align: ${spec.strokeAlign || 'inside'}`);
|
|
9034
|
+
}
|
|
9035
|
+
|
|
9036
|
+
// Typography
|
|
9037
|
+
if (spec.typography) {
|
|
9038
|
+
const t = spec.typography;
|
|
9039
|
+
lines.push(chalk.cyan('Typography'));
|
|
9040
|
+
if (t.fontFamily) lines.push(` Font: ${t.fontFamily} ${t.fontStyle || ''}`);
|
|
9041
|
+
if (t.fontSize) lines.push(` Size: ${dual(t.fontSize)}`);
|
|
9042
|
+
if (t.fontWeight) lines.push(` Weight: ${t.fontWeight}`);
|
|
9043
|
+
if (t.lineHeight) lines.push(` Line height: ${typeof t.lineHeight === 'string' ? t.lineHeight : dual(t.lineHeight)}`);
|
|
9044
|
+
if (t.letterSpacing) lines.push(` Letter spacing: ${dual(t.letterSpacing)}`);
|
|
9045
|
+
if (t.textAlign) lines.push(` Text align: ${t.textAlign}`);
|
|
9046
|
+
if (t.textDecoration) lines.push(` Decoration: ${t.textDecoration}`);
|
|
9047
|
+
if (t.textTransform) lines.push(` Transform: ${t.textTransform}`);
|
|
9048
|
+
if (t.content) lines.push(` Content: "${t.content.slice(0, 80)}${t.content.length > 80 ? '...' : ''}"`);
|
|
9049
|
+
}
|
|
9050
|
+
|
|
9051
|
+
// Effects
|
|
9052
|
+
if (spec.effects && spec.effects.length > 0) {
|
|
9053
|
+
lines.push(chalk.cyan('Effects'));
|
|
9054
|
+
for (const e of spec.effects) {
|
|
9055
|
+
if (e.type === 'DROP_SHADOW' || e.type === 'INNER_SHADOW') {
|
|
9056
|
+
lines.push(` ${e.type === 'DROP_SHADOW' ? 'Drop shadow' : 'Inner shadow'}: ${px(e.offset.x)}px ${px(e.offset.y)}px ${px(e.blur)}px ${px(e.spread)}px ${e.color || ''}`);
|
|
9057
|
+
lines.push(` → ${rem(e.offset.x)}rem ${rem(e.offset.y)}rem ${rem(e.blur)}rem ${rem(e.spread)}rem`);
|
|
9058
|
+
} else {
|
|
9059
|
+
lines.push(` ${e.type}: blur ${dual(e.blur)}`);
|
|
9060
|
+
}
|
|
9061
|
+
}
|
|
9062
|
+
}
|
|
9063
|
+
|
|
9064
|
+
// Opacity
|
|
9065
|
+
if (spec.opacity !== undefined) {
|
|
9066
|
+
lines.push(chalk.cyan('Opacity') + ` ${spec.opacity}`);
|
|
9067
|
+
}
|
|
9068
|
+
|
|
9069
|
+
// Variable bindings
|
|
9070
|
+
if (spec.variableBindings) {
|
|
9071
|
+
lines.push(chalk.cyan('Variable Bindings'));
|
|
9072
|
+
for (const [prop, binding] of Object.entries(spec.variableBindings)) {
|
|
9073
|
+
if (typeof binding === 'object' && binding.name) {
|
|
9074
|
+
let line = ` ${prop}: ${binding.name}`;
|
|
9075
|
+
if (binding.value !== undefined) line += ` = ${binding.value}`;
|
|
9076
|
+
if (binding.modes) {
|
|
9077
|
+
const modeStr = Object.entries(binding.modes).map(([m, v]) => `${m}: ${v}`).join(', ');
|
|
9078
|
+
line += ` [${modeStr}]`;
|
|
9079
|
+
}
|
|
9080
|
+
lines.push(line);
|
|
9081
|
+
} else {
|
|
9082
|
+
lines.push(` ${prop}: ${binding}`);
|
|
9083
|
+
}
|
|
9084
|
+
}
|
|
9085
|
+
}
|
|
9086
|
+
|
|
9087
|
+
// Component
|
|
9088
|
+
if (spec.component) {
|
|
9089
|
+
lines.push(chalk.cyan('Component'));
|
|
9090
|
+
lines.push(` Name: ${spec.component.name}`);
|
|
9091
|
+
if (spec.component.set) lines.push(` Set: ${spec.component.set}`);
|
|
9092
|
+
if (spec.component.isMainComponent) lines.push(` (Main component definition)`);
|
|
9093
|
+
}
|
|
9094
|
+
|
|
9095
|
+
// Children
|
|
9096
|
+
if (spec.childCount) {
|
|
9097
|
+
lines.push(chalk.cyan(`Children (${spec.childCount})`));
|
|
9098
|
+
for (const c of spec.children) {
|
|
9099
|
+
let line = ` [${c.type}] ${c.name}`;
|
|
9100
|
+
if (c.w && c.h) line += ` ${c.w}x${c.h}`;
|
|
9101
|
+
lines.push(line);
|
|
9102
|
+
}
|
|
9103
|
+
}
|
|
9104
|
+
|
|
9105
|
+
return lines.join('\n');
|
|
9106
|
+
}
|
|
9107
|
+
|
|
9108
|
+
// ============ CSS — generate CSS from selection ============
|
|
9109
|
+
|
|
9110
|
+
const CSS_GEN_CODE = `(function() {
|
|
9111
|
+
var sel = figma.currentPage.selection;
|
|
9112
|
+
if (!sel || sel.length === 0) return { error: 'Nothing selected. Select an element in Figma first.' };
|
|
9113
|
+
|
|
9114
|
+
var BASE = 16;
|
|
9115
|
+
function rem(px) { return parseFloat((px / BASE).toFixed(3)); }
|
|
9116
|
+
function colorHex(c) {
|
|
9117
|
+
var r = Math.round((c.r||0)*255), g = Math.round((c.g||0)*255), b = Math.round((c.b||0)*255);
|
|
9118
|
+
return '#' + r.toString(16).padStart(2,'0') + g.toString(16).padStart(2,'0') + b.toString(16).padStart(2,'0');
|
|
9119
|
+
}
|
|
9120
|
+
function colorRgba(c, a) {
|
|
9121
|
+
var r = Math.round((c.r||0)*255), g = Math.round((c.g||0)*255), b = Math.round((c.b||0)*255);
|
|
9122
|
+
if (a !== undefined && a < 1) return 'rgba(' + r + ',' + g + ',' + b + ',' + parseFloat(a.toFixed(2)) + ')';
|
|
9123
|
+
return 'rgb(' + r + ',' + g + ',' + b + ')';
|
|
9124
|
+
}
|
|
9125
|
+
|
|
9126
|
+
function genCSS(node, unit) {
|
|
9127
|
+
var props = {};
|
|
9128
|
+
var tw = [];
|
|
9129
|
+
|
|
9130
|
+
// Dimensions
|
|
9131
|
+
if (node.width !== undefined) {
|
|
9132
|
+
props['width'] = unit === 'rem' ? rem(node.width) + 'rem' : Math.round(node.width) + 'px';
|
|
9133
|
+
props['height'] = unit === 'rem' ? rem(node.height) + 'rem' : Math.round(node.height) + 'px';
|
|
9134
|
+
}
|
|
9135
|
+
|
|
9136
|
+
// Layout
|
|
9137
|
+
if (node.layoutMode && node.layoutMode !== 'NONE') {
|
|
9138
|
+
props['display'] = 'flex';
|
|
9139
|
+
props['flex-direction'] = node.layoutMode === 'HORIZONTAL' ? 'row' : 'column';
|
|
9140
|
+
if (node.itemSpacing) props['gap'] = unit === 'rem' ? rem(node.itemSpacing) + 'rem' : node.itemSpacing + 'px';
|
|
9141
|
+
|
|
9142
|
+
var pt = node.paddingTop || 0, pr = node.paddingRight || 0, pb = node.paddingBottom || 0, pl = node.paddingLeft || 0;
|
|
9143
|
+
if (pt || pr || pb || pl) {
|
|
9144
|
+
if (pt === pr && pr === pb && pb === pl) {
|
|
9145
|
+
props['padding'] = unit === 'rem' ? rem(pt) + 'rem' : pt + 'px';
|
|
9146
|
+
} else if (pt === pb && pl === pr) {
|
|
9147
|
+
props['padding'] = (unit === 'rem' ? rem(pt) + 'rem ' + rem(pr) + 'rem' : pt + 'px ' + pr + 'px');
|
|
9148
|
+
} else {
|
|
9149
|
+
props['padding'] = unit === 'rem'
|
|
9150
|
+
? rem(pt) + 'rem ' + rem(pr) + 'rem ' + rem(pb) + 'rem ' + rem(pl) + 'rem'
|
|
9151
|
+
: pt + 'px ' + pr + 'px ' + pb + 'px ' + pl + 'px';
|
|
9152
|
+
}
|
|
9153
|
+
}
|
|
9154
|
+
|
|
9155
|
+
var mainMap = { MIN: 'flex-start', CENTER: 'center', MAX: 'flex-end', SPACE_BETWEEN: 'space-between' };
|
|
9156
|
+
var crossMap = { MIN: 'flex-start', CENTER: 'center', MAX: 'flex-end' };
|
|
9157
|
+
if (node.primaryAxisAlignItems && node.primaryAxisAlignItems !== 'MIN') {
|
|
9158
|
+
props['justify-content'] = mainMap[node.primaryAxisAlignItems] || node.primaryAxisAlignItems;
|
|
9159
|
+
}
|
|
9160
|
+
if (node.counterAxisAlignItems && node.counterAxisAlignItems !== 'MIN') {
|
|
9161
|
+
props['align-items'] = crossMap[node.counterAxisAlignItems] || node.counterAxisAlignItems;
|
|
9162
|
+
}
|
|
9163
|
+
if (node.layoutWrap === 'WRAP') props['flex-wrap'] = 'wrap';
|
|
9164
|
+
}
|
|
9165
|
+
|
|
9166
|
+
// Border radius
|
|
9167
|
+
if (node.cornerRadius && node.cornerRadius !== 0 && typeof node.cornerRadius === 'number') {
|
|
9168
|
+
if (node.cornerRadius >= 9999) {
|
|
9169
|
+
props['border-radius'] = '9999px';
|
|
9170
|
+
} else {
|
|
9171
|
+
props['border-radius'] = unit === 'rem' ? rem(node.cornerRadius) + 'rem' : node.cornerRadius + 'px';
|
|
9172
|
+
}
|
|
9173
|
+
}
|
|
9174
|
+
if (node.topLeftRadius || node.topRightRadius || node.bottomRightRadius || node.bottomLeftRadius) {
|
|
9175
|
+
var tl = node.topLeftRadius||0, tr = node.topRightRadius||0, br = node.bottomRightRadius||0, bl = node.bottomLeftRadius||0;
|
|
9176
|
+
if (tl === tr && tr === br && br === bl) {
|
|
9177
|
+
props['border-radius'] = unit === 'rem' ? rem(tl) + 'rem' : tl + 'px';
|
|
9178
|
+
} else {
|
|
9179
|
+
props['border-radius'] = unit === 'rem'
|
|
9180
|
+
? rem(tl) + 'rem ' + rem(tr) + 'rem ' + rem(br) + 'rem ' + rem(bl) + 'rem'
|
|
9181
|
+
: tl + 'px ' + tr + 'px ' + br + 'px ' + bl + 'px';
|
|
9182
|
+
}
|
|
9183
|
+
}
|
|
9184
|
+
|
|
9185
|
+
// Fills
|
|
9186
|
+
if (node.fills && node.fills.length > 0) {
|
|
9187
|
+
for (var i = 0; i < node.fills.length; i++) {
|
|
9188
|
+
var f = node.fills[i];
|
|
9189
|
+
if (f.visible === false) continue;
|
|
9190
|
+
if (f.type === 'SOLID' && f.color) {
|
|
9191
|
+
var opacity = f.opacity !== undefined ? f.opacity : 1;
|
|
9192
|
+
props['background-color'] = opacity < 1 ? colorRgba(f.color, opacity) : colorHex(f.color);
|
|
9193
|
+
break;
|
|
9194
|
+
}
|
|
9195
|
+
if (f.type === 'GRADIENT_LINEAR' && f.gradientStops) {
|
|
9196
|
+
var stops = f.gradientStops.map(function(s) { return colorHex(s.color) + ' ' + Math.round(s.position*100) + '%'; });
|
|
9197
|
+
props['background'] = 'linear-gradient(' + stops.join(', ') + ')';
|
|
9198
|
+
break;
|
|
9199
|
+
}
|
|
9200
|
+
}
|
|
9201
|
+
}
|
|
9202
|
+
|
|
9203
|
+
// Strokes
|
|
9204
|
+
if (node.strokes && node.strokes.length > 0) {
|
|
9205
|
+
for (var i = 0; i < node.strokes.length; i++) {
|
|
9206
|
+
var s = node.strokes[i];
|
|
9207
|
+
if (s.visible === false) continue;
|
|
9208
|
+
if (s.type === 'SOLID' && s.color) {
|
|
9209
|
+
var sw = node.strokeWeight || 1;
|
|
9210
|
+
props['border'] = (unit === 'rem' ? rem(sw) + 'rem' : sw + 'px') + ' solid ' + colorHex(s.color);
|
|
9211
|
+
break;
|
|
9212
|
+
}
|
|
9213
|
+
}
|
|
9214
|
+
}
|
|
9215
|
+
|
|
9216
|
+
// Typography
|
|
9217
|
+
if (node.type === 'TEXT') {
|
|
9218
|
+
if (node.fontName && node.fontName !== figma.mixed) {
|
|
9219
|
+
props['font-family'] = "'" + node.fontName.family + "', sans-serif";
|
|
9220
|
+
}
|
|
9221
|
+
if (node.fontSize && node.fontSize !== figma.mixed) {
|
|
9222
|
+
props['font-size'] = unit === 'rem' ? rem(node.fontSize) + 'rem' : node.fontSize + 'px';
|
|
9223
|
+
}
|
|
9224
|
+
if (node.fontName && node.fontName !== figma.mixed && node.fontName.style) {
|
|
9225
|
+
var style = node.fontName.style.toLowerCase();
|
|
9226
|
+
if (style.includes('bold') || style.includes('700')) props['font-weight'] = 'bold';
|
|
9227
|
+
else if (style.includes('semibold') || style.includes('600')) props['font-weight'] = '600';
|
|
9228
|
+
else if (style.includes('medium') || style.includes('500')) props['font-weight'] = '500';
|
|
9229
|
+
else if (style.includes('light') || style.includes('300')) props['font-weight'] = '300';
|
|
9230
|
+
if (style.includes('italic')) props['font-style'] = 'italic';
|
|
9231
|
+
}
|
|
9232
|
+
if (node.lineHeight && node.lineHeight !== figma.mixed) {
|
|
9233
|
+
if (node.lineHeight.unit === 'PIXELS') {
|
|
9234
|
+
props['line-height'] = unit === 'rem' ? rem(node.lineHeight.value) + 'rem' : node.lineHeight.value + 'px';
|
|
9235
|
+
} else if (node.lineHeight.unit === 'PERCENT') {
|
|
9236
|
+
props['line-height'] = (node.lineHeight.value / 100).toFixed(2);
|
|
9237
|
+
}
|
|
9238
|
+
}
|
|
9239
|
+
if (node.letterSpacing && node.letterSpacing !== figma.mixed) {
|
|
9240
|
+
if (node.letterSpacing.unit === 'PIXELS' && node.letterSpacing.value !== 0) {
|
|
9241
|
+
props['letter-spacing'] = unit === 'rem' ? rem(node.letterSpacing.value) + 'rem' : node.letterSpacing.value + 'px';
|
|
9242
|
+
} else if (node.letterSpacing.unit === 'PERCENT' && node.letterSpacing.value !== 0) {
|
|
9243
|
+
props['letter-spacing'] = (node.letterSpacing.value / 100).toFixed(3) + 'em';
|
|
9244
|
+
}
|
|
9245
|
+
}
|
|
9246
|
+
if (node.textAlignHorizontal) {
|
|
9247
|
+
var alignMap = { LEFT: 'left', CENTER: 'center', RIGHT: 'right', JUSTIFIED: 'justify' };
|
|
9248
|
+
if (alignMap[node.textAlignHorizontal] && node.textAlignHorizontal !== 'LEFT') {
|
|
9249
|
+
props['text-align'] = alignMap[node.textAlignHorizontal];
|
|
9250
|
+
}
|
|
9251
|
+
}
|
|
9252
|
+
// Text fill color
|
|
9253
|
+
if (node.fills && node.fills.length > 0) {
|
|
9254
|
+
var tf = node.fills[0];
|
|
9255
|
+
if (tf.type === 'SOLID' && tf.color && tf.visible !== false) {
|
|
9256
|
+
props['color'] = colorHex(tf.color);
|
|
9257
|
+
delete props['background-color'];
|
|
9258
|
+
}
|
|
9259
|
+
}
|
|
9260
|
+
}
|
|
9261
|
+
|
|
9262
|
+
// Effects
|
|
9263
|
+
if (node.effects && node.effects.length > 0) {
|
|
9264
|
+
var shadows = [];
|
|
9265
|
+
for (var i = 0; i < node.effects.length; i++) {
|
|
9266
|
+
var e = node.effects[i];
|
|
9267
|
+
if (e.visible === false) continue;
|
|
9268
|
+
if (e.type === 'DROP_SHADOW') {
|
|
9269
|
+
var ox = unit === 'rem' ? rem(e.offset.x) + 'rem' : e.offset.x + 'px';
|
|
9270
|
+
var oy = unit === 'rem' ? rem(e.offset.y) + 'rem' : e.offset.y + 'px';
|
|
9271
|
+
var bl = unit === 'rem' ? rem(e.radius) + 'rem' : e.radius + 'px';
|
|
9272
|
+
var sp = unit === 'rem' ? rem(e.spread||0) + 'rem' : (e.spread||0) + 'px';
|
|
9273
|
+
shadows.push(ox + ' ' + oy + ' ' + bl + ' ' + sp + ' ' + (e.color ? colorRgba(e.color, e.color.a) : ''));
|
|
9274
|
+
}
|
|
9275
|
+
if (e.type === 'INNER_SHADOW') {
|
|
9276
|
+
var ox = unit === 'rem' ? rem(e.offset.x) + 'rem' : e.offset.x + 'px';
|
|
9277
|
+
var oy = unit === 'rem' ? rem(e.offset.y) + 'rem' : e.offset.y + 'px';
|
|
9278
|
+
var bl = unit === 'rem' ? rem(e.radius) + 'rem' : e.radius + 'px';
|
|
9279
|
+
var sp = unit === 'rem' ? rem(e.spread||0) + 'rem' : (e.spread||0) + 'px';
|
|
9280
|
+
shadows.push('inset ' + ox + ' ' + oy + ' ' + bl + ' ' + sp + ' ' + (e.color ? colorRgba(e.color, e.color.a) : ''));
|
|
9281
|
+
}
|
|
9282
|
+
}
|
|
9283
|
+
if (shadows.length > 0) props['box-shadow'] = shadows.join(', ');
|
|
9284
|
+
}
|
|
9285
|
+
|
|
9286
|
+
// Opacity
|
|
9287
|
+
if (node.opacity !== undefined && node.opacity < 1) {
|
|
9288
|
+
props['opacity'] = parseFloat(node.opacity.toFixed(2));
|
|
9289
|
+
}
|
|
9290
|
+
|
|
9291
|
+
// Overflow
|
|
9292
|
+
if (node.clipsContent) props['overflow'] = 'hidden';
|
|
9293
|
+
|
|
9294
|
+
return { name: node.name, type: node.type, css: props, text: node.type === 'TEXT' ? node.characters : undefined };
|
|
9295
|
+
}
|
|
9296
|
+
|
|
9297
|
+
var results = [];
|
|
9298
|
+
var count = Math.min(sel.length, 10);
|
|
9299
|
+
for (var i = 0; i < count; i++) {
|
|
9300
|
+
results.push(genCSS(sel[i], 'rem'));
|
|
9301
|
+
}
|
|
9302
|
+
return { count: sel.length, nodes: results };
|
|
9303
|
+
})()`;
|
|
9304
|
+
|
|
9305
|
+
program
|
|
9306
|
+
.command('css')
|
|
9307
|
+
.description('Generate CSS from the selected element — ready to paste into your codebase')
|
|
9308
|
+
.option('--px', 'Use px units instead of rem (default: rem)')
|
|
9309
|
+
.option('--tailwind', 'Output Tailwind classes instead of CSS')
|
|
9310
|
+
.option('--json', 'Output raw JSON')
|
|
9311
|
+
.option('--node <id>', 'Target a specific node by ID')
|
|
9312
|
+
.option('--link <url>', 'Target a node from a Figma selection link')
|
|
9313
|
+
.addHelpText('after', `
|
|
9314
|
+
Examples:
|
|
9315
|
+
fig css CSS for current selection (rem units)
|
|
9316
|
+
fig css --px CSS in px units
|
|
9317
|
+
fig css --tailwind Tailwind classes
|
|
9318
|
+
fig css --link "https://..." CSS from a Figma link
|
|
9319
|
+
`)
|
|
9320
|
+
.action(async (options) => {
|
|
9321
|
+
checkConnection();
|
|
9322
|
+
const spinner = ora('Generating CSS...').start();
|
|
9323
|
+
|
|
9324
|
+
try {
|
|
9325
|
+
let code = CSS_GEN_CODE;
|
|
9326
|
+
const unit = options.px ? 'px' : 'rem';
|
|
9327
|
+
code = code.replace("genCSS(sel[i], 'rem')", `genCSS(sel[i], '${unit}')`);
|
|
9328
|
+
|
|
9329
|
+
if (options.link) {
|
|
9330
|
+
const nodeId = parseNodeIdFromLink(options.link);
|
|
9331
|
+
if (!nodeId) { spinner.fail('Could not find a node ID in that link.'); process.exit(1); }
|
|
9332
|
+
code = code.replace(
|
|
9333
|
+
"var sel = figma.currentPage.selection;\n if (!sel || sel.length === 0) return { error: 'Nothing selected. Select an element in Figma first.' };",
|
|
9334
|
+
`var n = figma.getNodeById('${nodeId}'); if (!n) return { error: 'Node not found.' }; var sel = [n];`
|
|
9335
|
+
);
|
|
9336
|
+
} else if (options.node) {
|
|
9337
|
+
code = code.replace(
|
|
9338
|
+
"var sel = figma.currentPage.selection;\n if (!sel || sel.length === 0) return { error: 'Nothing selected. Select an element in Figma first.' };",
|
|
9339
|
+
`var n = figma.getNodeById('${options.node}'); if (!n) return { error: 'Node not found.' }; var sel = [n];`
|
|
9340
|
+
);
|
|
9341
|
+
}
|
|
9342
|
+
|
|
9343
|
+
const result = await daemonExec('eval', { code });
|
|
9344
|
+
|
|
9345
|
+
if (result.error) { spinner.fail(result.error); process.exit(1); }
|
|
9346
|
+
|
|
9347
|
+
spinner.succeed(`Generated CSS for ${result.nodes.length} element${result.nodes.length > 1 ? 's' : ''}`);
|
|
9348
|
+
console.log('');
|
|
9349
|
+
|
|
9350
|
+
if (options.json) {
|
|
9351
|
+
console.log(JSON.stringify(result, null, 2));
|
|
9352
|
+
return;
|
|
9353
|
+
}
|
|
9354
|
+
|
|
9355
|
+
for (const node of result.nodes) {
|
|
9356
|
+
if (options.tailwind) {
|
|
9357
|
+
console.log(formatTailwind(node));
|
|
9358
|
+
} else {
|
|
9359
|
+
console.log(formatCSS(node));
|
|
9360
|
+
}
|
|
9361
|
+
console.log('');
|
|
9362
|
+
}
|
|
9363
|
+
} catch (e) {
|
|
9364
|
+
spinner.fail(`CSS generation failed: ${e.message}`);
|
|
9365
|
+
process.exit(1);
|
|
9366
|
+
}
|
|
9367
|
+
});
|
|
9368
|
+
|
|
9369
|
+
function formatCSS(node) {
|
|
9370
|
+
const lines = [];
|
|
9371
|
+
const className = node.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
9372
|
+
lines.push(chalk.gray(`/* ${node.name} [${node.type}] */`));
|
|
9373
|
+
lines.push(`.${className} {`);
|
|
9374
|
+
for (const [prop, val] of Object.entries(node.css)) {
|
|
9375
|
+
lines.push(` ${prop}: ${val};`);
|
|
9376
|
+
}
|
|
9377
|
+
lines.push('}');
|
|
9378
|
+
if (node.text) {
|
|
9379
|
+
lines.push(chalk.gray(`/* Content: "${node.text.slice(0, 60)}${node.text.length > 60 ? '...' : ''}" */`));
|
|
9380
|
+
}
|
|
9381
|
+
return lines.join('\n');
|
|
9382
|
+
}
|
|
9383
|
+
|
|
9384
|
+
function formatTailwind(node) {
|
|
9385
|
+
const css = node.css;
|
|
9386
|
+
const classes = [];
|
|
9387
|
+
|
|
9388
|
+
// Width/height
|
|
9389
|
+
if (css['width']) classes.push(`w-[${css['width']}]`);
|
|
9390
|
+
if (css['height']) classes.push(`h-[${css['height']}]`);
|
|
9391
|
+
|
|
9392
|
+
// Display + flex
|
|
9393
|
+
if (css['display'] === 'flex') {
|
|
9394
|
+
classes.push('flex');
|
|
9395
|
+
if (css['flex-direction'] === 'column') classes.push('flex-col');
|
|
9396
|
+
if (css['flex-wrap'] === 'wrap') classes.push('flex-wrap');
|
|
9397
|
+
}
|
|
9398
|
+
|
|
9399
|
+
// Gap
|
|
9400
|
+
if (css['gap']) classes.push(`gap-[${css['gap']}]`);
|
|
9401
|
+
|
|
9402
|
+
// Padding
|
|
9403
|
+
if (css['padding']) {
|
|
9404
|
+
const parts = css['padding'].split(' ');
|
|
9405
|
+
if (parts.length === 1) classes.push(`p-[${parts[0]}]`);
|
|
9406
|
+
else if (parts.length === 2) { classes.push(`py-[${parts[0]}]`); classes.push(`px-[${parts[1]}]`); }
|
|
9407
|
+
else if (parts.length === 4) { classes.push(`pt-[${parts[0]}]`); classes.push(`pr-[${parts[1]}]`); classes.push(`pb-[${parts[2]}]`); classes.push(`pl-[${parts[3]}]`); }
|
|
9408
|
+
}
|
|
9409
|
+
|
|
9410
|
+
// Alignment
|
|
9411
|
+
if (css['justify-content']) {
|
|
9412
|
+
const map = { 'flex-start': 'justify-start', 'center': 'justify-center', 'flex-end': 'justify-end', 'space-between': 'justify-between' };
|
|
9413
|
+
classes.push(map[css['justify-content']] || `justify-[${css['justify-content']}]`);
|
|
9414
|
+
}
|
|
9415
|
+
if (css['align-items']) {
|
|
9416
|
+
const map = { 'flex-start': 'items-start', 'center': 'items-center', 'flex-end': 'items-end' };
|
|
9417
|
+
classes.push(map[css['align-items']] || `items-[${css['align-items']}]`);
|
|
9418
|
+
}
|
|
9419
|
+
|
|
9420
|
+
// Border radius
|
|
9421
|
+
if (css['border-radius']) {
|
|
9422
|
+
if (css['border-radius'] === '9999px') classes.push('rounded-full');
|
|
9423
|
+
else classes.push(`rounded-[${css['border-radius']}]`);
|
|
9424
|
+
}
|
|
9425
|
+
|
|
9426
|
+
// Background
|
|
9427
|
+
if (css['background-color']) classes.push(`bg-[${css['background-color']}]`);
|
|
9428
|
+
|
|
9429
|
+
// Border
|
|
9430
|
+
if (css['border']) classes.push(`border-[${css['border'].split(' solid ')[0]}]`, `border-[${css['border'].split(' solid ')[1] || ''}]`);
|
|
9431
|
+
|
|
9432
|
+
// Typography
|
|
9433
|
+
if (css['font-size']) classes.push(`text-[${css['font-size']}]`);
|
|
9434
|
+
if (css['font-weight']) {
|
|
9435
|
+
const map = { 'bold': 'font-bold', '600': 'font-semibold', '500': 'font-medium', '300': 'font-light' };
|
|
9436
|
+
classes.push(map[css['font-weight']] || `font-[${css['font-weight']}]`);
|
|
9437
|
+
}
|
|
9438
|
+
if (css['font-style'] === 'italic') classes.push('italic');
|
|
9439
|
+
if (css['line-height']) classes.push(`leading-[${css['line-height']}]`);
|
|
9440
|
+
if (css['letter-spacing']) classes.push(`tracking-[${css['letter-spacing']}]`);
|
|
9441
|
+
if (css['text-align']) {
|
|
9442
|
+
const map = { 'center': 'text-center', 'right': 'text-right', 'justify': 'text-justify' };
|
|
9443
|
+
classes.push(map[css['text-align']] || `text-${css['text-align']}`);
|
|
9444
|
+
}
|
|
9445
|
+
if (css['color']) classes.push(`text-[${css['color']}]`);
|
|
9446
|
+
|
|
9447
|
+
// Shadow
|
|
9448
|
+
if (css['box-shadow']) classes.push(`shadow-[${css['box-shadow'].replace(/\s+/g, '_')}]`);
|
|
9449
|
+
|
|
9450
|
+
// Opacity
|
|
9451
|
+
if (css['opacity']) classes.push(`opacity-[${css['opacity']}]`);
|
|
9452
|
+
|
|
9453
|
+
// Overflow
|
|
9454
|
+
if (css['overflow'] === 'hidden') classes.push('overflow-hidden');
|
|
9455
|
+
|
|
9456
|
+
const lines = [];
|
|
9457
|
+
lines.push(chalk.gray(`{/* ${node.name} */}`));
|
|
9458
|
+
lines.push(`className="${classes.join(' ')}"`);
|
|
9459
|
+
if (node.text) lines.push(chalk.gray(`{/* "${node.text.slice(0, 60)}${node.text.length > 60 ? '...' : ''}" */}`));
|
|
9460
|
+
return lines.join('\n');
|
|
9461
|
+
}
|
|
9462
|
+
|
|
9463
|
+
// ============ MEASURE — spacing between elements ============
|
|
9464
|
+
|
|
9465
|
+
program
|
|
9466
|
+
.command('measure')
|
|
9467
|
+
.description('Measure spacing between two selected elements — select exactly 2 layers in Figma')
|
|
9468
|
+
.option('--json', 'Output raw JSON')
|
|
9469
|
+
.action(async (options) => {
|
|
9470
|
+
checkConnection();
|
|
9471
|
+
const spinner = ora('Measuring spacing...').start();
|
|
9472
|
+
|
|
9473
|
+
try {
|
|
9474
|
+
const code = `(function() {
|
|
9475
|
+
var sel = figma.currentPage.selection;
|
|
9476
|
+
if (!sel || sel.length < 2) return { error: 'Select exactly 2 elements in Figma to measure the spacing between them.' };
|
|
9477
|
+
var a = sel[0], b = sel[1];
|
|
9478
|
+
var BASE = 16;
|
|
9479
|
+
function rem(px) { return parseFloat((px / BASE).toFixed(3)); }
|
|
9480
|
+
|
|
9481
|
+
var ax = a.absoluteTransform ? a.absoluteTransform[0][2] : a.x;
|
|
9482
|
+
var ay = a.absoluteTransform ? a.absoluteTransform[1][2] : a.y;
|
|
9483
|
+
var bx = b.absoluteTransform ? b.absoluteTransform[0][2] : b.x;
|
|
9484
|
+
var by = b.absoluteTransform ? b.absoluteTransform[1][2] : b.y;
|
|
9485
|
+
var aw = a.width, ah = a.height;
|
|
9486
|
+
var bw = b.width, bh = b.height;
|
|
9487
|
+
|
|
9488
|
+
var hGap = 0, vGap = 0;
|
|
9489
|
+
// Horizontal gap
|
|
9490
|
+
if (bx >= ax + aw) hGap = bx - (ax + aw);
|
|
9491
|
+
else if (ax >= bx + bw) hGap = ax - (bx + bw);
|
|
9492
|
+
// Vertical gap
|
|
9493
|
+
if (by >= ay + ah) vGap = by - (ay + ah);
|
|
9494
|
+
else if (ay >= by + bh) vGap = ay - (by + bh);
|
|
9495
|
+
|
|
9496
|
+
return {
|
|
9497
|
+
a: { name: a.name, x: Math.round(ax), y: Math.round(ay), w: Math.round(aw), h: Math.round(ah) },
|
|
9498
|
+
b: { name: b.name, x: Math.round(bx), y: Math.round(by), w: Math.round(bw), h: Math.round(bh) },
|
|
9499
|
+
spacing: {
|
|
9500
|
+
horizontal: { px: Math.round(hGap), rem: rem(hGap) },
|
|
9501
|
+
vertical: { px: Math.round(vGap), rem: rem(vGap) }
|
|
9502
|
+
},
|
|
9503
|
+
centerDistance: {
|
|
9504
|
+
x: { px: Math.round(Math.abs((bx + bw/2) - (ax + aw/2))), rem: rem(Math.abs((bx + bw/2) - (ax + aw/2))) },
|
|
9505
|
+
y: { px: Math.round(Math.abs((by + bh/2) - (ay + ah/2))), rem: rem(Math.abs((by + bh/2) - (ay + ah/2))) }
|
|
9506
|
+
}
|
|
9507
|
+
};
|
|
9508
|
+
})()`;
|
|
9509
|
+
|
|
9510
|
+
const result = await daemonExec('eval', { code });
|
|
9511
|
+
if (result.error) { spinner.fail(result.error); process.exit(1); }
|
|
9512
|
+
|
|
9513
|
+
spinner.succeed(`Measured: "${result.a.name}" ↔ "${result.b.name}"`);
|
|
9514
|
+
|
|
9515
|
+
if (options.json) {
|
|
9516
|
+
console.log(JSON.stringify(result, null, 2));
|
|
9517
|
+
} else {
|
|
9518
|
+
console.log('');
|
|
9519
|
+
console.log(chalk.bold(`${result.a.name}`) + chalk.gray(` (${result.a.w}x${result.a.h})`));
|
|
9520
|
+
console.log(chalk.gray(' ↕'));
|
|
9521
|
+
console.log(chalk.bold(`${result.b.name}`) + chalk.gray(` (${result.b.w}x${result.b.h})`));
|
|
9522
|
+
console.log('');
|
|
9523
|
+
console.log(chalk.cyan('Spacing'));
|
|
9524
|
+
console.log(` Horizontal: ${result.spacing.horizontal.px}px (${result.spacing.horizontal.rem}rem)`);
|
|
9525
|
+
console.log(` Vertical: ${result.spacing.vertical.px}px (${result.spacing.vertical.rem}rem)`);
|
|
9526
|
+
console.log(chalk.cyan('Center-to-center'));
|
|
9527
|
+
console.log(` X: ${result.centerDistance.x.px}px (${result.centerDistance.x.rem}rem)`);
|
|
9528
|
+
console.log(` Y: ${result.centerDistance.y.px}px (${result.centerDistance.y.rem}rem)`);
|
|
9529
|
+
}
|
|
9530
|
+
} catch (e) {
|
|
9531
|
+
spinner.fail(`Measure failed: ${e.message}`);
|
|
9532
|
+
process.exit(1);
|
|
9533
|
+
}
|
|
9534
|
+
});
|
|
9535
|
+
|
|
9536
|
+
// ============ STYLES — extract unique styles from a frame ============
|
|
9537
|
+
|
|
9538
|
+
program
|
|
9539
|
+
.command('styles [frameName]')
|
|
9540
|
+
.description('Extract all unique text styles, colors, and spacing values from a frame — a mini style guide')
|
|
9541
|
+
.option('--json', 'Output raw JSON')
|
|
9542
|
+
.option('--selection', 'Extract styles from current selection instead of a named frame')
|
|
9543
|
+
.option('--link <url>', 'Extract styles from a Figma selection link')
|
|
9544
|
+
.addHelpText('after', `
|
|
9545
|
+
Examples:
|
|
9546
|
+
fig styles "Login Screen" All text styles, colors, spacing in that frame
|
|
9547
|
+
fig styles --selection Styles from the current selection
|
|
9548
|
+
fig styles --json Raw JSON output
|
|
9549
|
+
`)
|
|
9550
|
+
.action(async (frameName, options) => {
|
|
9551
|
+
checkConnection();
|
|
9552
|
+
const spinner = ora('Extracting styles...').start();
|
|
9553
|
+
|
|
9554
|
+
try {
|
|
9555
|
+
let nodeSelector;
|
|
9556
|
+
if (options.selection) {
|
|
9557
|
+
nodeSelector = `var sel = figma.currentPage.selection; if (!sel || sel.length === 0) return { error: 'Nothing selected.' }; var root = sel[0];`;
|
|
9558
|
+
} else if (options.link) {
|
|
9559
|
+
const nodeId = parseNodeIdFromLink(options.link);
|
|
9560
|
+
if (!nodeId) { spinner.fail('Could not find a node ID in that link.'); process.exit(1); }
|
|
9561
|
+
nodeSelector = `var root = figma.getNodeById('${nodeId}'); if (!root) return { error: 'Node not found.' };`;
|
|
9562
|
+
} else if (frameName) {
|
|
9563
|
+
nodeSelector = `var frames = figma.currentPage.children.filter(function(n) { return n.name.toLowerCase().includes('${frameName.toLowerCase()}'); }); if (frames.length === 0) return { error: 'Frame "${frameName}" not found.' }; var root = frames[0];`;
|
|
9564
|
+
} else {
|
|
9565
|
+
nodeSelector = `var sel = figma.currentPage.selection; if (!sel || sel.length === 0) return { error: 'Select an element or specify a frame name.' }; var root = sel[0];`;
|
|
9566
|
+
}
|
|
9567
|
+
|
|
9568
|
+
const code = `(function() {
|
|
9569
|
+
${nodeSelector}
|
|
9570
|
+
var BASE = 16;
|
|
9571
|
+
function rem(px) { return parseFloat((px / BASE).toFixed(3)); }
|
|
9572
|
+
function colorHex(c) {
|
|
9573
|
+
var r = Math.round((c.r||0)*255), g = Math.round((c.g||0)*255), b = Math.round((c.b||0)*255);
|
|
9574
|
+
return '#' + r.toString(16).padStart(2,'0') + g.toString(16).padStart(2,'0') + b.toString(16).padStart(2,'0');
|
|
9575
|
+
}
|
|
9576
|
+
|
|
9577
|
+
var textStyles = {};
|
|
9578
|
+
var colors = {};
|
|
9579
|
+
var spacingValues = {};
|
|
9580
|
+
var radiusValues = {};
|
|
9581
|
+
|
|
9582
|
+
function walk(node) {
|
|
9583
|
+
// Colors from fills
|
|
9584
|
+
if (node.fills && node.fills.length > 0) {
|
|
9585
|
+
for (var i = 0; i < node.fills.length; i++) {
|
|
9586
|
+
var f = node.fills[i];
|
|
9587
|
+
if (f.visible === false) continue;
|
|
9588
|
+
if (f.type === 'SOLID' && f.color) {
|
|
9589
|
+
var hex = colorHex(f.color);
|
|
9590
|
+
if (!colors[hex]) colors[hex] = { count: 0, usedBy: [] };
|
|
9591
|
+
colors[hex].count++;
|
|
9592
|
+
if (colors[hex].usedBy.length < 3) colors[hex].usedBy.push(node.name);
|
|
9593
|
+
}
|
|
9594
|
+
}
|
|
9595
|
+
}
|
|
9596
|
+
|
|
9597
|
+
// Colors from strokes
|
|
9598
|
+
if (node.strokes && node.strokes.length > 0) {
|
|
9599
|
+
for (var i = 0; i < node.strokes.length; i++) {
|
|
9600
|
+
var s = node.strokes[i];
|
|
9601
|
+
if (s.visible === false) continue;
|
|
9602
|
+
if (s.type === 'SOLID' && s.color) {
|
|
9603
|
+
var hex = colorHex(s.color);
|
|
9604
|
+
if (!colors[hex]) colors[hex] = { count: 0, usedBy: [] };
|
|
9605
|
+
colors[hex].count++;
|
|
9606
|
+
}
|
|
9607
|
+
}
|
|
9608
|
+
}
|
|
9609
|
+
|
|
9610
|
+
// Typography
|
|
9611
|
+
if (node.type === 'TEXT') {
|
|
9612
|
+
var family = (node.fontName && node.fontName !== figma.mixed) ? node.fontName.family : 'mixed';
|
|
9613
|
+
var style = (node.fontName && node.fontName !== figma.mixed) ? node.fontName.style : '';
|
|
9614
|
+
var size = (node.fontSize && node.fontSize !== figma.mixed) ? node.fontSize : 0;
|
|
9615
|
+
var lh = '';
|
|
9616
|
+
if (node.lineHeight && node.lineHeight !== figma.mixed) {
|
|
9617
|
+
if (node.lineHeight.unit === 'PIXELS') lh = node.lineHeight.value + 'px';
|
|
9618
|
+
else if (node.lineHeight.unit === 'PERCENT') lh = node.lineHeight.value + '%';
|
|
9619
|
+
else lh = 'auto';
|
|
9620
|
+
}
|
|
9621
|
+
var key = family + '/' + style + '/' + size;
|
|
9622
|
+
if (!textStyles[key]) {
|
|
9623
|
+
textStyles[key] = { fontFamily: family, fontStyle: style, fontSize: size, lineHeight: lh, count: 0 };
|
|
9624
|
+
}
|
|
9625
|
+
textStyles[key].count++;
|
|
9626
|
+
}
|
|
9627
|
+
|
|
9628
|
+
// Spacing (gap, padding)
|
|
9629
|
+
if (node.layoutMode && node.layoutMode !== 'NONE') {
|
|
9630
|
+
if (node.itemSpacing) {
|
|
9631
|
+
var g = Math.round(node.itemSpacing);
|
|
9632
|
+
spacingValues[g + 'px'] = (spacingValues[g + 'px'] || 0) + 1;
|
|
9633
|
+
}
|
|
9634
|
+
var sides = [node.paddingTop, node.paddingRight, node.paddingBottom, node.paddingLeft];
|
|
9635
|
+
sides.forEach(function(v) {
|
|
9636
|
+
if (v && v > 0) {
|
|
9637
|
+
var k = Math.round(v) + 'px';
|
|
9638
|
+
spacingValues[k] = (spacingValues[k] || 0) + 1;
|
|
9639
|
+
}
|
|
9640
|
+
});
|
|
9641
|
+
}
|
|
9642
|
+
|
|
9643
|
+
// Radius
|
|
9644
|
+
if (node.cornerRadius && node.cornerRadius !== 0 && typeof node.cornerRadius === 'number') {
|
|
9645
|
+
var rk = node.cornerRadius >= 9999 ? 'full' : Math.round(node.cornerRadius) + 'px';
|
|
9646
|
+
radiusValues[rk] = (radiusValues[rk] || 0) + 1;
|
|
9647
|
+
}
|
|
9648
|
+
|
|
9649
|
+
if (node.children) {
|
|
9650
|
+
for (var i = 0; i < node.children.length; i++) walk(node.children[i]);
|
|
9651
|
+
}
|
|
9652
|
+
}
|
|
9653
|
+
walk(root);
|
|
9654
|
+
|
|
9655
|
+
return {
|
|
9656
|
+
name: root.name,
|
|
9657
|
+
textStyles: Object.values(textStyles).sort(function(a,b) { return b.count - a.count; }),
|
|
9658
|
+
colors: Object.keys(colors).map(function(hex) { return { hex: hex, count: colors[hex].count, usedBy: colors[hex].usedBy }; }).sort(function(a,b) { return b.count - a.count; }),
|
|
9659
|
+
spacing: Object.keys(spacingValues).map(function(k) { var px = parseInt(k); return { value: k, rem: rem(px) + 'rem', count: spacingValues[k] }; }).sort(function(a,b) { return parseInt(a.value) - parseInt(b.value); }),
|
|
9660
|
+
radii: Object.keys(radiusValues).map(function(k) { var px = k === 'full' ? 9999 : parseInt(k); return { value: k, rem: k === 'full' ? 'full' : rem(px) + 'rem', count: radiusValues[k] }; }).sort(function(a,b) { return parseInt(a.value) - parseInt(b.value); })
|
|
9661
|
+
};
|
|
9662
|
+
})()`;
|
|
9663
|
+
|
|
9664
|
+
const result = await daemonExec('eval', { code });
|
|
9665
|
+
if (result.error) { spinner.fail(result.error); process.exit(1); }
|
|
9666
|
+
|
|
9667
|
+
spinner.succeed(`Styles extracted from "${result.name}"`);
|
|
9668
|
+
|
|
9669
|
+
if (options.json) {
|
|
9670
|
+
console.log(JSON.stringify(result, null, 2));
|
|
9671
|
+
return;
|
|
9672
|
+
}
|
|
9673
|
+
|
|
9674
|
+
console.log('');
|
|
9675
|
+
// Text styles
|
|
9676
|
+
if (result.textStyles.length > 0) {
|
|
9677
|
+
console.log(chalk.cyan.bold(`Typography (${result.textStyles.length} styles)`));
|
|
9678
|
+
for (const t of result.textStyles) {
|
|
9679
|
+
let line = ` ${t.fontFamily} ${t.fontStyle} ${t.fontSize}px (${(t.fontSize/16).toFixed(3)}rem)`;
|
|
9680
|
+
if (t.lineHeight) line += ` line-height: ${t.lineHeight}`;
|
|
9681
|
+
line += chalk.gray(` ×${t.count}`);
|
|
9682
|
+
console.log(line);
|
|
9683
|
+
}
|
|
9684
|
+
console.log('');
|
|
9685
|
+
}
|
|
9686
|
+
|
|
9687
|
+
// Colors
|
|
9688
|
+
if (result.colors.length > 0) {
|
|
9689
|
+
console.log(chalk.cyan.bold(`Colors (${result.colors.length} unique)`));
|
|
9690
|
+
for (const c of result.colors) {
|
|
9691
|
+
let line = ` ${c.hex}`;
|
|
9692
|
+
if (c.usedBy.length > 0) line += chalk.gray(` used by: ${c.usedBy.join(', ')}`);
|
|
9693
|
+
line += chalk.gray(` ×${c.count}`);
|
|
9694
|
+
console.log(line);
|
|
9695
|
+
}
|
|
9696
|
+
console.log('');
|
|
9697
|
+
}
|
|
9698
|
+
|
|
9699
|
+
// Spacing scale
|
|
9700
|
+
if (result.spacing.length > 0) {
|
|
9701
|
+
console.log(chalk.cyan.bold(`Spacing scale (${result.spacing.length} values)`));
|
|
9702
|
+
for (const s of result.spacing) {
|
|
9703
|
+
console.log(` ${s.value.padEnd(8)} ${s.rem.padEnd(10)}` + chalk.gray(`×${s.count}`));
|
|
9704
|
+
}
|
|
9705
|
+
console.log('');
|
|
9706
|
+
}
|
|
9707
|
+
|
|
9708
|
+
// Radii
|
|
9709
|
+
if (result.radii.length > 0) {
|
|
9710
|
+
console.log(chalk.cyan.bold(`Border radii (${result.radii.length} values)`));
|
|
9711
|
+
for (const r of result.radii) {
|
|
9712
|
+
console.log(` ${r.value.padEnd(8)} ${r.rem.padEnd(10)}` + chalk.gray(`×${r.count}`));
|
|
9713
|
+
}
|
|
9714
|
+
}
|
|
9715
|
+
} catch (e) {
|
|
9716
|
+
spinner.fail(`Styles extraction failed: ${e.message}`);
|
|
9717
|
+
process.exit(1);
|
|
9718
|
+
}
|
|
9719
|
+
});
|
|
9720
|
+
|
|
9721
|
+
// ============ DOCUMENT — deep component documentation ============
|
|
9722
|
+
|
|
9723
|
+
const DOCUMENT_CODE = `(function() {
|
|
9724
|
+
var BASE = 16;
|
|
9725
|
+
function toRem(px) { return parseFloat((px / BASE).toFixed(3)); }
|
|
9726
|
+
function dual(px) { return { px: Math.round(px), rem: toRem(px) }; }
|
|
9727
|
+
function colorToHex(c) {
|
|
9728
|
+
var r = Math.round((c.r||0)*255), g = Math.round((c.g||0)*255), b = Math.round((c.b||0)*255);
|
|
9729
|
+
return '#' + r.toString(16).padStart(2,'0') + g.toString(16).padStart(2,'0') + b.toString(16).padStart(2,'0');
|
|
9730
|
+
}
|
|
9731
|
+
function colorToRgba(c, opacity) {
|
|
9732
|
+
var r = Math.round((c.r||0)*255), g = Math.round((c.g||0)*255), b = Math.round((c.b||0)*255);
|
|
9733
|
+
if (opacity !== undefined && opacity < 1) return 'rgba(' + r + ',' + g + ',' + b + ',' + parseFloat(opacity.toFixed(2)) + ')';
|
|
9734
|
+
return 'rgb(' + r + ',' + g + ',' + b + ')';
|
|
9735
|
+
}
|
|
9736
|
+
|
|
9737
|
+
function extractNode(node, depth) {
|
|
9738
|
+
if (depth > 15) return null;
|
|
9739
|
+
var n = { name: node.name, type: node.type };
|
|
9740
|
+
|
|
9741
|
+
// Dimensions
|
|
9742
|
+
if (node.width !== undefined) {
|
|
9743
|
+
n.width = dual(node.width);
|
|
9744
|
+
n.height = dual(node.height);
|
|
9745
|
+
}
|
|
9746
|
+
|
|
9747
|
+
// Visibility & opacity
|
|
9748
|
+
if (node.visible === false) n.visible = false;
|
|
9749
|
+
if (node.opacity !== undefined && node.opacity < 1) n.opacity = parseFloat(node.opacity.toFixed(2));
|
|
9750
|
+
|
|
9751
|
+
// Auto layout
|
|
9752
|
+
if (node.layoutMode && node.layoutMode !== 'NONE') {
|
|
9753
|
+
n.layout = {
|
|
9754
|
+
direction: node.layoutMode === 'HORIZONTAL' ? 'row' : 'column',
|
|
9755
|
+
gap: dual(node.itemSpacing || 0),
|
|
9756
|
+
padding: {
|
|
9757
|
+
top: dual(node.paddingTop || 0),
|
|
9758
|
+
right: dual(node.paddingRight || 0),
|
|
9759
|
+
bottom: dual(node.paddingBottom || 0),
|
|
9760
|
+
left: dual(node.paddingLeft || 0)
|
|
9761
|
+
},
|
|
9762
|
+
mainAxisAlign: node.primaryAxisAlignItems || 'MIN',
|
|
9763
|
+
crossAxisAlign: node.counterAxisAlignItems || 'MIN'
|
|
9764
|
+
};
|
|
9765
|
+
if (node.layoutWrap === 'WRAP') n.layout.wrap = true;
|
|
9766
|
+
if (node.primaryAxisSizingMode) n.layout.mainAxisSizing = node.primaryAxisSizingMode;
|
|
9767
|
+
if (node.counterAxisSizingMode) n.layout.crossAxisSizing = node.counterAxisSizingMode;
|
|
9768
|
+
}
|
|
9769
|
+
|
|
9770
|
+
// Sizing behavior
|
|
9771
|
+
if (node.layoutSizingHorizontal) n.horizontalSizing = node.layoutSizingHorizontal;
|
|
9772
|
+
if (node.layoutSizingVertical) n.verticalSizing = node.layoutSizingVertical;
|
|
9773
|
+
|
|
9774
|
+
// Corner radius
|
|
9775
|
+
if (node.cornerRadius !== undefined && node.cornerRadius !== 0 && typeof node.cornerRadius === 'number') {
|
|
9776
|
+
n.borderRadius = dual(node.cornerRadius);
|
|
9777
|
+
} else if (node.topLeftRadius || node.topRightRadius || node.bottomRightRadius || node.bottomLeftRadius) {
|
|
9778
|
+
n.borderRadius = {
|
|
9779
|
+
topLeft: dual(node.topLeftRadius || 0),
|
|
9780
|
+
topRight: dual(node.topRightRadius || 0),
|
|
9781
|
+
bottomRight: dual(node.bottomRightRadius || 0),
|
|
9782
|
+
bottomLeft: dual(node.bottomLeftRadius || 0)
|
|
9783
|
+
};
|
|
9784
|
+
}
|
|
9785
|
+
|
|
9786
|
+
// Fills
|
|
9787
|
+
if (node.fills && node.fills.length > 0) {
|
|
9788
|
+
n.fills = [];
|
|
9789
|
+
for (var i = 0; i < node.fills.length; i++) {
|
|
9790
|
+
var f = node.fills[i];
|
|
9791
|
+
if (f.visible === false) continue;
|
|
9792
|
+
var fill = { type: f.type };
|
|
9793
|
+
if (f.type === 'SOLID' && f.color) {
|
|
9794
|
+
fill.hex = colorToHex(f.color);
|
|
9795
|
+
fill.rgba = colorToRgba(f.color, f.opacity);
|
|
9796
|
+
if (f.opacity !== undefined && f.opacity < 1) fill.opacity = parseFloat(f.opacity.toFixed(2));
|
|
9797
|
+
}
|
|
9798
|
+
if (f.type === 'GRADIENT_LINEAR' || f.type === 'GRADIENT_RADIAL') {
|
|
9799
|
+
fill.stops = f.gradientStops ? f.gradientStops.map(function(s) {
|
|
9800
|
+
return { color: colorToHex(s.color), position: parseFloat(s.position.toFixed(2)) };
|
|
9801
|
+
}) : [];
|
|
9802
|
+
}
|
|
9803
|
+
if (f.type === 'IMAGE') fill.scaleMode = f.scaleMode;
|
|
9804
|
+
n.fills.push(fill);
|
|
9805
|
+
}
|
|
9806
|
+
if (n.fills.length === 0) delete n.fills;
|
|
9807
|
+
}
|
|
9808
|
+
|
|
9809
|
+
// Strokes
|
|
9810
|
+
if (node.strokes && node.strokes.length > 0) {
|
|
9811
|
+
n.strokes = [];
|
|
9812
|
+
for (var i = 0; i < node.strokes.length; i++) {
|
|
9813
|
+
var s = node.strokes[i];
|
|
9814
|
+
if (s.visible === false) continue;
|
|
9815
|
+
var stroke = { type: s.type };
|
|
9816
|
+
if (s.type === 'SOLID' && s.color) {
|
|
9817
|
+
stroke.hex = colorToHex(s.color);
|
|
9818
|
+
stroke.rgba = colorToRgba(s.color, s.opacity);
|
|
9819
|
+
}
|
|
9820
|
+
n.strokes.push(stroke);
|
|
9821
|
+
}
|
|
9822
|
+
if (n.strokes.length === 0) delete n.strokes;
|
|
9823
|
+
if (node.strokeWeight) n.strokeWeight = dual(node.strokeWeight);
|
|
9824
|
+
if (node.strokeAlign) n.strokeAlign = node.strokeAlign;
|
|
9825
|
+
}
|
|
9826
|
+
|
|
9827
|
+
// Typography
|
|
9828
|
+
if (node.type === 'TEXT') {
|
|
9829
|
+
n.text = { content: node.characters };
|
|
9830
|
+
if (node.fontName && node.fontName !== figma.mixed) {
|
|
9831
|
+
n.text.fontFamily = node.fontName.family;
|
|
9832
|
+
n.text.fontStyle = node.fontName.style;
|
|
9833
|
+
}
|
|
9834
|
+
if (node.fontSize && node.fontSize !== figma.mixed) n.text.fontSize = dual(node.fontSize);
|
|
9835
|
+
if (node.fontWeight && node.fontWeight !== figma.mixed) n.text.fontWeight = node.fontWeight;
|
|
9836
|
+
if (node.lineHeight && node.lineHeight !== figma.mixed) {
|
|
9837
|
+
if (node.lineHeight.unit === 'PIXELS') n.text.lineHeight = dual(node.lineHeight.value);
|
|
9838
|
+
else if (node.lineHeight.unit === 'PERCENT') n.text.lineHeight = node.lineHeight.value + '%';
|
|
9839
|
+
else n.text.lineHeight = 'auto';
|
|
9840
|
+
}
|
|
9841
|
+
if (node.letterSpacing && node.letterSpacing !== figma.mixed) {
|
|
9842
|
+
if (node.letterSpacing.unit === 'PIXELS') n.text.letterSpacing = dual(node.letterSpacing.value);
|
|
9843
|
+
else if (node.letterSpacing.unit === 'PERCENT') n.text.letterSpacing = node.letterSpacing.value + '%';
|
|
9844
|
+
}
|
|
9845
|
+
if (node.textAlignHorizontal) n.text.textAlign = node.textAlignHorizontal.toLowerCase();
|
|
9846
|
+
if (node.textDecoration && node.textDecoration !== 'NONE' && node.textDecoration !== figma.mixed) {
|
|
9847
|
+
n.text.textDecoration = node.textDecoration.toLowerCase();
|
|
9848
|
+
}
|
|
9849
|
+
if (node.textCase && node.textCase !== 'ORIGINAL' && node.textCase !== figma.mixed) {
|
|
9850
|
+
n.text.textTransform = node.textCase.toLowerCase();
|
|
9851
|
+
}
|
|
9852
|
+
}
|
|
9853
|
+
|
|
9854
|
+
// Effects
|
|
9855
|
+
if (node.effects && node.effects.length > 0) {
|
|
9856
|
+
n.effects = [];
|
|
9857
|
+
for (var i = 0; i < node.effects.length; i++) {
|
|
9858
|
+
var e = node.effects[i];
|
|
9859
|
+
if (e.visible === false) continue;
|
|
9860
|
+
var eff = { type: e.type };
|
|
9861
|
+
if (e.type === 'DROP_SHADOW' || e.type === 'INNER_SHADOW') {
|
|
9862
|
+
eff.x = dual(e.offset.x); eff.y = dual(e.offset.y);
|
|
9863
|
+
eff.blur = dual(e.radius); eff.spread = dual(e.spread || 0);
|
|
9864
|
+
if (e.color) eff.color = colorToRgba(e.color, e.color.a);
|
|
9865
|
+
}
|
|
9866
|
+
if (e.type === 'LAYER_BLUR' || e.type === 'BACKGROUND_BLUR') {
|
|
9867
|
+
eff.blur = dual(e.radius);
|
|
9868
|
+
}
|
|
9869
|
+
n.effects.push(eff);
|
|
9870
|
+
}
|
|
9871
|
+
if (n.effects.length === 0) delete n.effects;
|
|
9872
|
+
}
|
|
9873
|
+
|
|
9874
|
+
// Constraints
|
|
9875
|
+
if (node.constraints) {
|
|
9876
|
+
n.constraints = { horizontal: node.constraints.horizontal, vertical: node.constraints.vertical };
|
|
9877
|
+
}
|
|
9878
|
+
|
|
9879
|
+
// Clip content
|
|
9880
|
+
if (node.clipsContent) n.clipsContent = true;
|
|
9881
|
+
|
|
9882
|
+
// Component info
|
|
9883
|
+
if (node.type === 'INSTANCE' && node.mainComponent) {
|
|
9884
|
+
n.component = { name: node.mainComponent.name, id: node.mainComponent.id };
|
|
9885
|
+
if (node.mainComponent.parent && node.mainComponent.parent.type === 'COMPONENT_SET') {
|
|
9886
|
+
n.component.set = node.mainComponent.parent.name;
|
|
9887
|
+
}
|
|
9888
|
+
// Overridden properties
|
|
9889
|
+
try {
|
|
9890
|
+
var overrides = node.overrides;
|
|
9891
|
+
if (overrides && overrides.length > 0) {
|
|
9892
|
+
n.component.overrideCount = overrides.length;
|
|
9893
|
+
}
|
|
9894
|
+
} catch(e) {}
|
|
9895
|
+
}
|
|
9896
|
+
if (node.type === 'COMPONENT') {
|
|
9897
|
+
n.component = { name: node.name, id: node.id, isDefinition: true };
|
|
9898
|
+
if (node.description) n.component.description = node.description;
|
|
9899
|
+
}
|
|
9900
|
+
|
|
9901
|
+
// Variable bindings
|
|
9902
|
+
if (node.boundVariables) {
|
|
9903
|
+
var vars = {};
|
|
9904
|
+
var bindings = node.boundVariables;
|
|
9905
|
+
Object.keys(bindings).forEach(function(prop) {
|
|
9906
|
+
var binding = bindings[prop];
|
|
9907
|
+
if (!binding) return;
|
|
9908
|
+
var entries = Array.isArray(binding) ? binding : [binding];
|
|
9909
|
+
entries.forEach(function(b) {
|
|
9910
|
+
if (!b || !b.id) return;
|
|
9911
|
+
try {
|
|
9912
|
+
var v = figma.variables.getVariableById(b.id);
|
|
9913
|
+
if (v) {
|
|
9914
|
+
var info = { name: v.name };
|
|
9915
|
+
try {
|
|
9916
|
+
var modeIds = Object.keys(v.valuesByMode);
|
|
9917
|
+
if (modeIds.length > 0) {
|
|
9918
|
+
var val = v.valuesByMode[modeIds[0]];
|
|
9919
|
+
if (val !== undefined) {
|
|
9920
|
+
if (typeof val === 'object' && val.r !== undefined) {
|
|
9921
|
+
info.value = colorToHex(val);
|
|
9922
|
+
} else if (typeof val === 'object' && val.id) {
|
|
9923
|
+
try {
|
|
9924
|
+
var alias = figma.variables.getVariableById(val.id);
|
|
9925
|
+
if (alias) info.value = alias.name + ' (alias)';
|
|
9926
|
+
} catch(e2) { info.value = 'alias'; }
|
|
9927
|
+
} else {
|
|
9928
|
+
info.value = val;
|
|
9929
|
+
}
|
|
9930
|
+
}
|
|
9931
|
+
}
|
|
9932
|
+
if (modeIds.length > 1) {
|
|
9933
|
+
info.modes = {};
|
|
9934
|
+
for (var mi = 0; mi < modeIds.length; mi++) {
|
|
9935
|
+
var mVal = v.valuesByMode[modeIds[mi]];
|
|
9936
|
+
try {
|
|
9937
|
+
var collection = figma.variables.getVariableCollectionById(v.variableCollectionId);
|
|
9938
|
+
var modeName = collection ? collection.modes.find(function(m) { return m.modeId === modeIds[mi]; }) : null;
|
|
9939
|
+
var mKey = modeName ? modeName.name : modeIds[mi];
|
|
9940
|
+
if (typeof mVal === 'object' && mVal.r !== undefined) {
|
|
9941
|
+
info.modes[mKey] = colorToHex(mVal);
|
|
9942
|
+
} else if (typeof mVal === 'object' && mVal.id) {
|
|
9943
|
+
try {
|
|
9944
|
+
var aliasM = figma.variables.getVariableById(mVal.id);
|
|
9945
|
+
info.modes[mKey] = aliasM ? aliasM.name : 'alias';
|
|
9946
|
+
} catch(e3) { info.modes[mKey] = 'alias'; }
|
|
9947
|
+
} else {
|
|
9948
|
+
info.modes[mKey] = mVal;
|
|
9949
|
+
}
|
|
9950
|
+
} catch(e4) {}
|
|
9951
|
+
}
|
|
9952
|
+
}
|
|
9953
|
+
} catch(e5) {}
|
|
9954
|
+
vars[prop] = info;
|
|
9955
|
+
}
|
|
9956
|
+
} catch(e) {}
|
|
9957
|
+
});
|
|
9958
|
+
});
|
|
9959
|
+
if (Object.keys(vars).length > 0) n.variables = vars;
|
|
9960
|
+
}
|
|
9961
|
+
|
|
9962
|
+
// Children — recurse
|
|
9963
|
+
if (node.children && node.children.length > 0) {
|
|
9964
|
+
n.children = [];
|
|
9965
|
+
for (var i = 0; i < node.children.length; i++) {
|
|
9966
|
+
var child = extractNode(node.children[i], depth + 1);
|
|
9967
|
+
if (child) n.children.push(child);
|
|
9968
|
+
}
|
|
9969
|
+
}
|
|
9970
|
+
|
|
9971
|
+
return n;
|
|
9972
|
+
}
|
|
9973
|
+
|
|
9974
|
+
// Collect unique tokens used across the tree
|
|
9975
|
+
function collectTokens(tree) {
|
|
9976
|
+
var colors = {};
|
|
9977
|
+
var fonts = {};
|
|
9978
|
+
var spacings = {};
|
|
9979
|
+
var radii = {};
|
|
9980
|
+
var shadows = {};
|
|
9981
|
+
|
|
9982
|
+
function walk(n) {
|
|
9983
|
+
// Colors from fills
|
|
9984
|
+
if (n.fills) n.fills.forEach(function(f) {
|
|
9985
|
+
if (f.hex) colors[f.hex] = (colors[f.hex] || 0) + 1;
|
|
9986
|
+
});
|
|
9987
|
+
// Colors from strokes
|
|
9988
|
+
if (n.strokes) n.strokes.forEach(function(s) {
|
|
9989
|
+
if (s.hex) colors[s.hex] = (colors[s.hex] || 0) + 1;
|
|
9990
|
+
});
|
|
9991
|
+
// Typography
|
|
9992
|
+
if (n.text) {
|
|
9993
|
+
if (n.text.fontFamily) {
|
|
9994
|
+
var key = n.text.fontFamily + ' ' + (n.text.fontStyle || 'Regular');
|
|
9995
|
+
if (n.text.fontSize) key += ' / ' + n.text.fontSize.px + 'px';
|
|
9996
|
+
fonts[key] = (fonts[key] || 0) + 1;
|
|
9997
|
+
}
|
|
9998
|
+
}
|
|
9999
|
+
// Spacing from layout
|
|
10000
|
+
if (n.layout) {
|
|
10001
|
+
if (n.layout.gap && n.layout.gap.px > 0) spacings[n.layout.gap.px + 'px'] = (spacings[n.layout.gap.px + 'px'] || 0) + 1;
|
|
10002
|
+
if (n.layout.padding) {
|
|
10003
|
+
var p = n.layout.padding;
|
|
10004
|
+
[p.top, p.right, p.bottom, p.left].forEach(function(v) {
|
|
10005
|
+
if (v && v.px > 0) spacings[v.px + 'px'] = (spacings[v.px + 'px'] || 0) + 1;
|
|
10006
|
+
});
|
|
10007
|
+
}
|
|
10008
|
+
}
|
|
10009
|
+
// Radii
|
|
10010
|
+
if (n.borderRadius) {
|
|
10011
|
+
if (n.borderRadius.px !== undefined) {
|
|
10012
|
+
radii[n.borderRadius.px + 'px'] = (radii[n.borderRadius.px + 'px'] || 0) + 1;
|
|
10013
|
+
} else {
|
|
10014
|
+
[n.borderRadius.topLeft, n.borderRadius.topRight, n.borderRadius.bottomRight, n.borderRadius.bottomLeft].forEach(function(v) {
|
|
10015
|
+
if (v && v.px > 0) radii[v.px + 'px'] = (radii[v.px + 'px'] || 0) + 1;
|
|
10016
|
+
});
|
|
10017
|
+
}
|
|
10018
|
+
}
|
|
10019
|
+
// Shadows
|
|
10020
|
+
if (n.effects) n.effects.forEach(function(e) {
|
|
10021
|
+
if (e.type === 'DROP_SHADOW' || e.type === 'INNER_SHADOW') {
|
|
10022
|
+
var key = e.x.px + 'px ' + e.y.px + 'px ' + e.blur.px + 'px ' + e.spread.px + 'px ' + (e.color || '');
|
|
10023
|
+
shadows[key] = (shadows[key] || 0) + 1;
|
|
10024
|
+
}
|
|
10025
|
+
});
|
|
10026
|
+
// Recurse
|
|
10027
|
+
if (n.children) n.children.forEach(walk);
|
|
10028
|
+
}
|
|
10029
|
+
walk(tree);
|
|
10030
|
+
|
|
10031
|
+
return {
|
|
10032
|
+
colors: Object.keys(colors).sort(function(a,b) { return colors[b] - colors[a]; }).map(function(c) { return { value: c, count: colors[c] }; }),
|
|
10033
|
+
typography: Object.keys(fonts).sort(function(a,b) { return fonts[b] - fonts[a]; }).map(function(f) { return { value: f, count: fonts[f] }; }),
|
|
10034
|
+
spacing: Object.keys(spacings).sort(function(a,b) { return parseInt(a) - parseInt(b); }).map(function(s) { return { value: s, rem: toRem(parseInt(s)) + 'rem', count: spacings[s] }; }),
|
|
10035
|
+
radii: Object.keys(radii).sort(function(a,b) { return parseInt(a) - parseInt(b); }).map(function(r) { return { value: r, rem: toRem(parseInt(r)) + 'rem', count: radii[r] }; }),
|
|
10036
|
+
shadows: Object.keys(shadows).map(function(s) { return { value: s, count: shadows[s] }; })
|
|
10037
|
+
};
|
|
10038
|
+
}
|
|
10039
|
+
|
|
10040
|
+
// Count total nodes
|
|
10041
|
+
function countNodes(n) {
|
|
10042
|
+
var c = 1;
|
|
10043
|
+
if (n.children) n.children.forEach(function(ch) { c += countNodes(ch); });
|
|
10044
|
+
return c;
|
|
10045
|
+
}
|
|
10046
|
+
|
|
10047
|
+
__TARGET_SELECTOR__
|
|
10048
|
+
|
|
10049
|
+
var tree = extractNode(node, 0);
|
|
10050
|
+
var tokens = collectTokens(tree);
|
|
10051
|
+
var total = countNodes(tree);
|
|
10052
|
+
|
|
10053
|
+
return {
|
|
10054
|
+
summary: {
|
|
10055
|
+
name: node.name,
|
|
10056
|
+
type: node.type,
|
|
10057
|
+
totalNodes: total,
|
|
10058
|
+
width: node.width ? Math.round(node.width) : undefined,
|
|
10059
|
+
height: node.height ? Math.round(node.height) : undefined,
|
|
10060
|
+
isComponent: node.type === 'COMPONENT' || node.type === 'COMPONENT_SET',
|
|
10061
|
+
isInstance: node.type === 'INSTANCE'
|
|
10062
|
+
},
|
|
10063
|
+
tokens: tokens,
|
|
10064
|
+
tree: tree
|
|
10065
|
+
};
|
|
10066
|
+
})()`;
|
|
10067
|
+
|
|
10068
|
+
function buildDocumentCode(target) {
|
|
10069
|
+
if (target === 'selection') {
|
|
10070
|
+
return DOCUMENT_CODE.replace('__TARGET_SELECTOR__',
|
|
10071
|
+
"var sel = figma.currentPage.selection;\n if (!sel || sel.length === 0) return { error: 'Nothing selected. Select a component or frame in Figma first.' };\n var node = sel[0];"
|
|
10072
|
+
);
|
|
10073
|
+
}
|
|
10074
|
+
// target is a node ID
|
|
10075
|
+
return DOCUMENT_CODE.replace('__TARGET_SELECTOR__',
|
|
10076
|
+
`var node = figma.getNodeById('${target}');\n if (!node) return { error: 'Node ${target} not found.' };`
|
|
10077
|
+
);
|
|
10078
|
+
}
|
|
10079
|
+
|
|
10080
|
+
function formatDocument(doc) {
|
|
10081
|
+
const lines = [];
|
|
10082
|
+
|
|
10083
|
+
// Header
|
|
10084
|
+
lines.push(chalk.bold.underline(`Component Documentation: ${doc.summary.name}`));
|
|
10085
|
+
lines.push('');
|
|
10086
|
+
|
|
10087
|
+
// Summary
|
|
10088
|
+
lines.push(chalk.bold('Overview'));
|
|
10089
|
+
lines.push(` Type: ${doc.summary.type}`);
|
|
10090
|
+
lines.push(` Size: ${doc.summary.width}×${doc.summary.height}px`);
|
|
10091
|
+
lines.push(` Total nodes: ${doc.summary.totalNodes}`);
|
|
10092
|
+
if (doc.summary.isComponent) lines.push(` Role: Component definition`);
|
|
10093
|
+
if (doc.summary.isInstance) lines.push(` Role: Component instance`);
|
|
10094
|
+
lines.push('');
|
|
10095
|
+
|
|
10096
|
+
// Design tokens summary
|
|
10097
|
+
lines.push(chalk.bold('Design Tokens'));
|
|
10098
|
+
lines.push('');
|
|
10099
|
+
|
|
10100
|
+
if (doc.tokens.colors.length > 0) {
|
|
10101
|
+
lines.push(chalk.cyan(' Colors'));
|
|
10102
|
+
for (const c of doc.tokens.colors) {
|
|
10103
|
+
lines.push(` ${c.value} (used ${c.count}×)`);
|
|
10104
|
+
}
|
|
10105
|
+
}
|
|
10106
|
+
|
|
10107
|
+
if (doc.tokens.typography.length > 0) {
|
|
10108
|
+
lines.push(chalk.cyan(' Typography'));
|
|
10109
|
+
for (const t of doc.tokens.typography) {
|
|
10110
|
+
lines.push(` ${t.value} (${t.count}×)`);
|
|
10111
|
+
}
|
|
10112
|
+
}
|
|
10113
|
+
|
|
10114
|
+
if (doc.tokens.spacing.length > 0) {
|
|
10115
|
+
lines.push(chalk.cyan(' Spacing'));
|
|
10116
|
+
lines.push(` ${doc.tokens.spacing.map(s => `${s.value} (${s.rem})`).join(' | ')}`);
|
|
10117
|
+
}
|
|
10118
|
+
|
|
10119
|
+
if (doc.tokens.radii.length > 0) {
|
|
10120
|
+
lines.push(chalk.cyan(' Border Radii'));
|
|
10121
|
+
lines.push(` ${doc.tokens.radii.map(r => `${r.value} (${r.rem})`).join(' | ')}`);
|
|
10122
|
+
}
|
|
10123
|
+
|
|
10124
|
+
if (doc.tokens.shadows.length > 0) {
|
|
10125
|
+
lines.push(chalk.cyan(' Shadows'));
|
|
10126
|
+
for (const s of doc.tokens.shadows) {
|
|
10127
|
+
lines.push(` ${s.value}`);
|
|
10128
|
+
}
|
|
10129
|
+
}
|
|
10130
|
+
|
|
10131
|
+
lines.push('');
|
|
10132
|
+
lines.push(chalk.bold('Component Tree (JSON)'));
|
|
10133
|
+
lines.push('');
|
|
10134
|
+
lines.push(JSON.stringify(doc.tree, null, 2));
|
|
10135
|
+
|
|
10136
|
+
return lines.join('\n');
|
|
10137
|
+
}
|
|
10138
|
+
|
|
10139
|
+
program
|
|
10140
|
+
.command('document')
|
|
10141
|
+
.description('Generate complete component documentation — recursive specs, tokens, and structured tree for coding agents')
|
|
10142
|
+
.option('--json', 'Output raw JSON only (best for coding agents)')
|
|
10143
|
+
.option('--node <id>', 'Document a specific node by ID')
|
|
10144
|
+
.option('--link <url>', 'Document from a Figma selection link')
|
|
10145
|
+
.option('--selection', 'Document the current selection (default)')
|
|
10146
|
+
.option('--tokens-only', 'Output only the design tokens summary')
|
|
10147
|
+
.addHelpText('after', `
|
|
10148
|
+
Examples:
|
|
10149
|
+
fig document Document the selected component
|
|
10150
|
+
fig document --json JSON output for coding agents
|
|
10151
|
+
fig document --node "123:456" Document a specific node
|
|
10152
|
+
fig document --link "https://..." Document from a Figma link
|
|
10153
|
+
fig document --tokens-only Just the design tokens used
|
|
10154
|
+
`)
|
|
10155
|
+
.action(async (options) => {
|
|
10156
|
+
checkConnection();
|
|
10157
|
+
const spinner = ora('Documenting component...').start();
|
|
10158
|
+
|
|
10159
|
+
try {
|
|
10160
|
+
let target = 'selection';
|
|
10161
|
+
|
|
10162
|
+
if (options.link) {
|
|
10163
|
+
const nodeId = parseNodeIdFromLink(options.link);
|
|
10164
|
+
if (!nodeId) {
|
|
10165
|
+
spinner.fail('Could not find a node ID in that link. Copy via right-click → "Copy link to selection" in Figma.');
|
|
10166
|
+
process.exit(1);
|
|
10167
|
+
}
|
|
10168
|
+
target = nodeId;
|
|
10169
|
+
} else if (options.node) {
|
|
10170
|
+
target = options.node;
|
|
10171
|
+
}
|
|
10172
|
+
|
|
10173
|
+
const code = buildDocumentCode(target);
|
|
10174
|
+
const result = await daemonExec('eval', { code }, 120000);
|
|
10175
|
+
|
|
10176
|
+
if (result.error) {
|
|
10177
|
+
spinner.fail(result.error);
|
|
10178
|
+
process.exit(1);
|
|
10179
|
+
}
|
|
10180
|
+
|
|
10181
|
+
spinner.succeed(`Documented "${result.summary.name}" — ${result.summary.totalNodes} nodes`);
|
|
10182
|
+
|
|
10183
|
+
if (options.tokensOnly) {
|
|
10184
|
+
console.log(JSON.stringify(result.tokens, null, 2));
|
|
10185
|
+
} else if (options.json) {
|
|
10186
|
+
console.log(JSON.stringify(result, null, 2));
|
|
10187
|
+
} else {
|
|
10188
|
+
console.log('');
|
|
10189
|
+
console.log(formatDocument(result));
|
|
10190
|
+
}
|
|
10191
|
+
} catch (e) {
|
|
10192
|
+
spinner.fail(`Documentation failed: ${e.message}`);
|
|
10193
|
+
process.exit(1);
|
|
10194
|
+
}
|
|
10195
|
+
});
|
|
10196
|
+
|
|
8450
10197
|
// ============ PROMPT — export to AI tool ============
|
|
8451
10198
|
|
|
8452
10199
|
program
|
|
8453
10200
|
.command('prompt [frameName]')
|
|
8454
|
-
.description('
|
|
10201
|
+
.description('Export a frame as a text prompt for AI tools (Figma Make, Lovable, Pencil, Stitch)')
|
|
8455
10202
|
.option('-t, --target <tool>', 'Target tool: figma-make | lovable | pencil | paper | stitch', 'figma-make')
|
|
8456
10203
|
.option('-p, --platform <platform>', 'desktop | mobile | responsive', 'desktop')
|
|
8457
10204
|
.option('-s, --stack <stack>', 'Tech stack (for Lovable/Pencil)', 'React + shadcn/ui + Tailwind')
|