@sangheepark/figma-ds-mcp 0.2.5 → 0.2.6

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.
@@ -277,13 +277,13 @@ function enrichSpec(traversal, mapping) {
277
277
  }
278
278
  }
279
279
  walk(spec, 'root');
280
- // Step 6-C: root frame sizing (fill 1440px width, hug height)
280
+ // Step 6-C: root frame sizing (viewport defaults: 1440×900 PC)
281
281
  if (spec.layout) {
282
282
  if (spec.layout.width === 'fill') {
283
283
  spec.layout.width = '1440';
284
284
  }
285
285
  if (spec.layout.height === 'fill') {
286
- spec.layout.height = 'hug';
286
+ spec.layout.height = '900';
287
287
  }
288
288
  }
289
289
  return { spec, errors };
@@ -467,6 +467,80 @@ function gateCheck(spec) {
467
467
  const pass = results.every(r => r.pass);
468
468
  return { pass, results };
469
469
  }
470
+ // --- _repeat: Component Auto-Extraction ---
471
+ function extractRepeatComponents(spec, defaultCount = 3) {
472
+ const extracted = new Map(); // name → component spec
473
+ function deepClone(obj) {
474
+ return JSON.parse(JSON.stringify(obj));
475
+ }
476
+ function getRepeatCount(node) {
477
+ if (typeof node._repeat === 'number' && node._repeat > 0)
478
+ return node._repeat;
479
+ return defaultCount;
480
+ }
481
+ // Depth-first walk: process children first so nested _repeat resolves before parent
482
+ function walk(node) {
483
+ // Process children first (depth-first)
484
+ if (node.children) {
485
+ for (const child of node.children) {
486
+ walk(child);
487
+ }
488
+ }
489
+ // Now process this node's children for _repeat
490
+ if (!node.children)
491
+ return;
492
+ const newChildren = [];
493
+ for (const child of node.children) {
494
+ if (!child._repeat) {
495
+ newChildren.push(child);
496
+ continue;
497
+ }
498
+ const count = getRepeatCount(child);
499
+ const nodeAny = child;
500
+ const hasLibraryKey = !!nodeAny['library-key'];
501
+ const hasDs = !!child._ds;
502
+ // Library instance: just duplicate N times, no component extraction
503
+ if (hasLibraryKey || hasDs) {
504
+ const cleanChild = deepClone(child);
505
+ delete cleanChild._repeat;
506
+ for (let i = 0; i < count; i++) {
507
+ const instance = deepClone(cleanChild);
508
+ if (instance.name)
509
+ instance.name = `${instance.name}_${i + 1}`;
510
+ newChildren.push(instance);
511
+ }
512
+ continue;
513
+ }
514
+ // Custom frame: extract as component + insert instances
515
+ const compName = child.name || `RepeatComponent_${extracted.size + 1}`;
516
+ if (!extracted.has(compName)) {
517
+ // Extract as component
518
+ const compSpec = deepClone(child);
519
+ compSpec.type = 'component';
520
+ compSpec.name = compName;
521
+ delete compSpec._repeat;
522
+ extracted.set(compName, compSpec);
523
+ }
524
+ // Insert N instances in place
525
+ for (let i = 0; i < count; i++) {
526
+ newChildren.push({
527
+ type: 'instance',
528
+ component: compName,
529
+ name: `${compName}_${i + 1}`,
530
+ });
531
+ }
532
+ }
533
+ node.children = newChildren;
534
+ }
535
+ const page = deepClone(spec);
536
+ // Skip if root itself has _repeat (standalone component, no extraction needed)
537
+ if (page._repeat) {
538
+ delete page._repeat;
539
+ return { page, components: [] };
540
+ }
541
+ walk(page);
542
+ return { page, components: Array.from(extracted.values()) };
543
+ }
470
544
  // fontWeight name → CSS number (reverse of display names in data files)
471
545
  const FONT_WEIGHT_NAME_TO_NUM = {
472
546
  thin: '100', hairline: '100',
@@ -993,9 +1067,16 @@ If no reviewNeeded, completes in 1 pass.`, {
993
1067
  }],
994
1068
  };
995
1069
  }
1070
+ // Step 3.5: Extract _repeat components
1071
+ // Components are placed before pages so builder creates them first
1072
+ const finalSpecs = [];
1073
+ for (const spec of specs) {
1074
+ const { page, components } = extractRepeatComponents(spec);
1075
+ finalSpecs.push(...components, page);
1076
+ }
996
1077
  // Step 4: Gate check each spec
997
1078
  const gateResults = [];
998
- for (const spec of specs) {
1079
+ for (const spec of finalSpecs) {
999
1080
  gateResults.push(gateCheck(spec));
1000
1081
  }
1001
1082
  const allGatesPass = gateResults.every(g => g.pass);
@@ -1003,18 +1084,18 @@ If no reviewNeeded, completes in 1 pass.`, {
1003
1084
  mkdirSync(dirname(outputPath), { recursive: true });
1004
1085
  const trees = [];
1005
1086
  const allStats = [];
1006
- if (specs.length === 1) {
1007
- writeFileSync(outputPath, JSON.stringify(specs[0], null, 2));
1008
- trees.push(specToTree(specs[0]));
1009
- allStats.push(countSpecNodes(specs[0]));
1087
+ if (finalSpecs.length === 1) {
1088
+ writeFileSync(outputPath, JSON.stringify(finalSpecs[0], null, 2));
1089
+ trees.push(specToTree(finalSpecs[0]));
1090
+ allStats.push(countSpecNodes(finalSpecs[0]));
1010
1091
  }
1011
1092
  else {
1012
1093
  // Multiple specs → save each with index
1013
- for (let i = 0; i < specs.length; i++) {
1094
+ for (let i = 0; i < finalSpecs.length; i++) {
1014
1095
  const specPath = outputPath.replace(/\.json$/, `_${i}.json`);
1015
- writeFileSync(specPath, JSON.stringify(specs[i], null, 2));
1016
- trees.push(specToTree(specs[i]));
1017
- allStats.push(countSpecNodes(specs[i]));
1096
+ writeFileSync(specPath, JSON.stringify(finalSpecs[i], null, 2));
1097
+ trees.push(specToTree(finalSpecs[i]));
1098
+ allStats.push(countSpecNodes(finalSpecs[i]));
1018
1099
  }
1019
1100
  }
1020
1101
  // Collect unresolved
@@ -57,20 +57,47 @@ function analyzeLayoutWarnings(spec) {
57
57
  return warnings;
58
58
  }
59
59
  export function registerUtilityTools(server, bridge) {
60
- // get_status — Check connection status
60
+ // get_status — Check connection status + bridge version/capabilities
61
61
  server.tool('get_status', 'Check if the Figma Bridge plugin is connected and ready', {}, async () => {
62
62
  const connected = bridge.isConnected();
63
- return {
64
- content: [{
65
- type: 'text',
66
- text: JSON.stringify({
67
- connected,
68
- message: connected
69
- ? 'Figma Bridge plugin is connected and ready.'
70
- : 'Figma Bridge plugin is NOT connected. Please open the "Code to Figma Bridge" plugin in Figma.',
71
- }, null, 2),
72
- }],
73
- };
63
+ if (!connected) {
64
+ return {
65
+ content: [{
66
+ type: 'text',
67
+ text: JSON.stringify({
68
+ connected: false,
69
+ message: 'Figma Bridge plugin is NOT connected. Please open the "Code to Figma Bridge" plugin in Figma.',
70
+ }, null, 2),
71
+ }],
72
+ };
73
+ }
74
+ try {
75
+ const bridgeStatus = await bridge.send('bridge-get-status');
76
+ return {
77
+ content: [{
78
+ type: 'text',
79
+ text: JSON.stringify({
80
+ connected: true,
81
+ message: 'Figma Bridge plugin is connected and ready.',
82
+ bridge: {
83
+ version: bridgeStatus.version || 'unknown',
84
+ capabilities: bridgeStatus.capabilities || [],
85
+ },
86
+ }, null, 2),
87
+ }],
88
+ };
89
+ }
90
+ catch {
91
+ return {
92
+ content: [{
93
+ type: 'text',
94
+ text: JSON.stringify({
95
+ connected: true,
96
+ message: 'Figma Bridge plugin is connected but bridge-get-status failed. Bridge plugin dist may need rebuild.',
97
+ }, null, 2),
98
+ }],
99
+ };
100
+ }
74
101
  });
75
102
  // validate_spec — Validate a JSON spec without creating anything
76
103
  server.tool('validate_spec', 'Validate a component/variable/style spec (JSON) without creating anything in Figma. Returns validation errors if any.', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sangheepark/figma-ds-mcp",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "MCP server for Code to Figma Bridge — bridges Claude Code to Figma plugin via WebSocket",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",