@sangheepark/figma-ds-mcp 0.2.4 → 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.
@@ -112,18 +112,52 @@ function enrichSpec(traversal, mapping) {
112
112
  if (component) {
113
113
  node['library-key'] = component['library-key'];
114
114
  // Step 2: _ds_props → variant-props + overrides 분리
115
+ // value 역검색 + default fallback 로직 포함
115
116
  if (node._ds_props) {
116
117
  const variantProps = {};
117
118
  const overrides = {};
118
119
  for (const [key, value] of Object.entries(node._ds_props)) {
119
120
  if (component.variants && key in component.variants) {
120
- // variant axis에 해당하는 prop
121
+ // 1) exact key match → variant-props
121
122
  if (typeof value === 'string' && component.defaults?.[key] !== value) {
122
123
  variantProps[key] = value;
123
124
  }
124
125
  }
126
+ else if (component.variants && typeof value === 'string') {
127
+ // 2) exact match 실패 → value 역검색: 이 값을 가진 variant axis 찾기
128
+ const matchingAxes = [];
129
+ for (const [axis, options] of Object.entries(component.variants)) {
130
+ if (axis in variantProps)
131
+ continue; // 이미 매핑된 axis 스킵
132
+ if (options.includes(value) && component.defaults?.[axis] !== value) {
133
+ matchingAxes.push(axis);
134
+ }
135
+ }
136
+ if (matchingAxes.length === 1) {
137
+ // 정확히 1개 axis에서만 발견 → 매핑
138
+ variantProps[matchingAxes[0]] = value;
139
+ }
140
+ // 0개 또는 2개 이상 → 매핑 포기 (default variant 사용)
141
+ }
142
+ else if (component.properties && typeof value === 'string') {
143
+ // 3) variant가 아닌 prop → property name 역검색
144
+ if (key in component.properties) {
145
+ // exact property name match
146
+ overrides[key] = value;
147
+ }
148
+ else {
149
+ // exact match 실패 → TEXT property 자동 매핑
150
+ const textProps = Object.entries(component.properties)
151
+ .filter(([, type]) => type === 'TEXT')
152
+ .map(([name]) => name);
153
+ if (textProps.length === 1) {
154
+ overrides[textProps[0]] = value;
155
+ }
156
+ // 0개 또는 2개 이상 → 매핑 포기
157
+ }
158
+ }
125
159
  else {
126
- // variant가 아닌 prop → overrides (text, boolean, swap)
160
+ // boolean 기타 → overrides 직접
127
161
  overrides[key] = value;
128
162
  }
129
163
  }
@@ -222,6 +256,19 @@ function enrichSpec(traversal, mapping) {
222
256
  node.layout.direction = 'column';
223
257
  }
224
258
  }
259
+ // 6-D: absolute + fill 정규화
260
+ // Figma에서 absolute 요소는 parent auto-layout에 참여하지 않으므로
261
+ // fill은 layoutSizingHorizontal/Vertical = 'FILL'로 처리 → constraints STRETCH 동작
262
+ // 별도 변환 불필요 — NodeBuilder가 absolute child에 fill을 적용하면 STRETCH로 동작
263
+ // 다만 x/y가 없으면 0으로 기본값 설정하여 parent 좌상단 정렬
264
+ if (node.layout?.positioning === 'absolute') {
265
+ if ((node.layout.width === 'fill' || node.layout.width === '100%') && node.layout.x === undefined) {
266
+ node.layout.x = '0';
267
+ }
268
+ if ((node.layout.height === 'fill' || node.layout.height === '100%') && node.layout.y === undefined) {
269
+ node.layout.y = '0';
270
+ }
271
+ }
225
272
  // children 재귀
226
273
  if (node.children) {
227
274
  node.children.forEach((child, i) => {
@@ -230,13 +277,13 @@ function enrichSpec(traversal, mapping) {
230
277
  }
231
278
  }
232
279
  walk(spec, 'root');
233
- // Step 6-C: root frame sizing (fill 1440px width, hug height)
280
+ // Step 6-C: root frame sizing (viewport defaults: 1440×900 PC)
234
281
  if (spec.layout) {
235
282
  if (spec.layout.width === 'fill') {
236
283
  spec.layout.width = '1440';
237
284
  }
238
285
  if (spec.layout.height === 'fill') {
239
- spec.layout.height = 'hug';
286
+ spec.layout.height = '900';
240
287
  }
241
288
  }
242
289
  return { spec, errors };
@@ -420,6 +467,80 @@ function gateCheck(spec) {
420
467
  const pass = results.every(r => r.pass);
421
468
  return { pass, results };
422
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
+ }
423
544
  // fontWeight name → CSS number (reverse of display names in data files)
424
545
  const FONT_WEIGHT_NAME_TO_NUM = {
425
546
  thin: '100', hairline: '100',
@@ -946,9 +1067,16 @@ If no reviewNeeded, completes in 1 pass.`, {
946
1067
  }],
947
1068
  };
948
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
+ }
949
1077
  // Step 4: Gate check each spec
950
1078
  const gateResults = [];
951
- for (const spec of specs) {
1079
+ for (const spec of finalSpecs) {
952
1080
  gateResults.push(gateCheck(spec));
953
1081
  }
954
1082
  const allGatesPass = gateResults.every(g => g.pass);
@@ -956,18 +1084,18 @@ If no reviewNeeded, completes in 1 pass.`, {
956
1084
  mkdirSync(dirname(outputPath), { recursive: true });
957
1085
  const trees = [];
958
1086
  const allStats = [];
959
- if (specs.length === 1) {
960
- writeFileSync(outputPath, JSON.stringify(specs[0], null, 2));
961
- trees.push(specToTree(specs[0]));
962
- 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]));
963
1091
  }
964
1092
  else {
965
1093
  // Multiple specs → save each with index
966
- for (let i = 0; i < specs.length; i++) {
1094
+ for (let i = 0; i < finalSpecs.length; i++) {
967
1095
  const specPath = outputPath.replace(/\.json$/, `_${i}.json`);
968
- writeFileSync(specPath, JSON.stringify(specs[i], null, 2));
969
- trees.push(specToTree(specs[i]));
970
- 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]));
971
1099
  }
972
1100
  }
973
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.4",
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",