@atomixstudio/mcp 1.0.34 → 1.0.35
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 +10 -2
- package/dist/chunk-426RNS3G.js +1782 -0
- package/dist/chunk-426RNS3G.js.map +1 -0
- package/dist/figma-bridge-protocol.d.ts +4 -3
- package/dist/figma-bridge-protocol.js +1 -1
- package/dist/index.js +534 -742
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-CE6J5MJX.js +0 -49
- package/dist/chunk-CE6J5MJX.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
FIGMA_DESIGN_CATALOG,
|
|
4
|
+
FIGMA_DESIGN_SKILL_MD,
|
|
5
|
+
buildFigmaPayloadsFromDS,
|
|
6
|
+
buildResolvers,
|
|
7
|
+
formatCatalogForMCP,
|
|
8
|
+
getDesignMethodNames,
|
|
9
|
+
getQueryMethodNames,
|
|
3
10
|
isAllowedMethod,
|
|
4
|
-
normalizeBridgeMethod
|
|
5
|
-
|
|
11
|
+
normalizeBridgeMethod,
|
|
12
|
+
resolveStepParams
|
|
13
|
+
} from "./chunk-426RNS3G.js";
|
|
6
14
|
|
|
7
15
|
// src/index.ts
|
|
8
16
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -222,7 +230,7 @@ function detectGovernanceChangesByFoundation(cached, fresh) {
|
|
|
222
230
|
return changes;
|
|
223
231
|
}
|
|
224
232
|
async function fetchDesignSystem(options) {
|
|
225
|
-
const { dsId: dsId2, apiKey: apiKey2, accessToken: accessToken2, apiBase: apiBase2 = "https://
|
|
233
|
+
const { dsId: dsId2, apiKey: apiKey2, accessToken: accessToken2, apiBase: apiBase2 = "https://atomix.studio", etag, forceRefresh = false } = options;
|
|
226
234
|
if (!dsId2) {
|
|
227
235
|
throw new Error("Missing dsId. Usage: fetchDesignSystem({ dsId: '...' })");
|
|
228
236
|
}
|
|
@@ -1093,9 +1101,7 @@ function formatSyncResponse(options) {
|
|
|
1093
1101
|
`;
|
|
1094
1102
|
});
|
|
1095
1103
|
response += `
|
|
1096
|
-
These changes are reflected in
|
|
1097
|
-
`;
|
|
1098
|
-
response += `Review the updated rules files to see the new guidance.
|
|
1104
|
+
These changes are reflected in the design system governance; run /--sync to update the skill and call getRules for the latest rules.
|
|
1099
1105
|
`;
|
|
1100
1106
|
}
|
|
1101
1107
|
if (diff) {
|
|
@@ -1218,709 +1224,6 @@ function getTokenStats(data) {
|
|
|
1218
1224
|
};
|
|
1219
1225
|
}
|
|
1220
1226
|
|
|
1221
|
-
// ../figma-tools/dist/index.js
|
|
1222
|
-
var LEGACY_GROUP_PREFIX = {
|
|
1223
|
-
background: "bg",
|
|
1224
|
-
text: "text",
|
|
1225
|
-
icon: "icon",
|
|
1226
|
-
border: "border",
|
|
1227
|
-
brand: "brand",
|
|
1228
|
-
action: "action",
|
|
1229
|
-
feedback: "feedback"
|
|
1230
|
-
};
|
|
1231
|
-
var RESERVED_COLOR_KEYS = /* @__PURE__ */ new Set(["customScales", "neutral"]);
|
|
1232
|
-
function getColorGroupOrder(storedColors) {
|
|
1233
|
-
if (Array.isArray(storedColors._groupOrder) && storedColors._groupOrder.length > 0) {
|
|
1234
|
-
return storedColors._groupOrder;
|
|
1235
|
-
}
|
|
1236
|
-
return Object.keys(storedColors).filter(
|
|
1237
|
-
(k) => !k.startsWith("_") && !RESERVED_COLOR_KEYS.has(k) && typeof storedColors[k] === "object"
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
function getGroupPrefix(storedColors, groupName) {
|
|
1241
|
-
const custom = storedColors._groupPrefix;
|
|
1242
|
-
if (custom && typeof custom[groupName] === "string") return custom[groupName];
|
|
1243
|
-
return LEGACY_GROUP_PREFIX[groupName] ?? groupName;
|
|
1244
|
-
}
|
|
1245
|
-
function refScaleToFigmaDisplayName(scaleFromRef) {
|
|
1246
|
-
const s = scaleFromRef.trim();
|
|
1247
|
-
if (!s) return s;
|
|
1248
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1249
|
-
}
|
|
1250
|
-
function referenceToFigmaPrimitiveName(ref, primitiveNames) {
|
|
1251
|
-
if (!ref || typeof ref !== "string") return null;
|
|
1252
|
-
const r = ref.trim();
|
|
1253
|
-
const scaleStep = /^([a-zA-Z]+)\.(\d+|[a-z]+)$/.exec(r);
|
|
1254
|
-
if (scaleStep) {
|
|
1255
|
-
const [, scale, step] = scaleStep;
|
|
1256
|
-
const scaleDisplay = refScaleToFigmaDisplayName(scale);
|
|
1257
|
-
const name = `${scaleDisplay} / ${step}`;
|
|
1258
|
-
return primitiveNames.has(name) ? name : null;
|
|
1259
|
-
}
|
|
1260
|
-
const radixMatch = /^radix\.([a-zA-Z]+)\.(\d+)$/.exec(r);
|
|
1261
|
-
if (radixMatch) {
|
|
1262
|
-
const [, family, step] = radixMatch;
|
|
1263
|
-
const scaleName = refScaleToFigmaDisplayName(family);
|
|
1264
|
-
const name = `${scaleName} / ${step}`;
|
|
1265
|
-
return primitiveNames.has(name) ? name : null;
|
|
1266
|
-
}
|
|
1267
|
-
const oneOffMatch = /^(?:new|oneOff)\.(.+)$/.exec(r);
|
|
1268
|
-
if (oneOffMatch) {
|
|
1269
|
-
const name = `One-off / ${oneOffMatch[1]}`;
|
|
1270
|
-
return primitiveNames.has(name) ? name : null;
|
|
1271
|
-
}
|
|
1272
|
-
const whiteAlpha = /^whiteAlpha\.(.+)$/.exec(r);
|
|
1273
|
-
if (whiteAlpha) {
|
|
1274
|
-
const name = `WhiteAlpha / ${whiteAlpha[1]}`;
|
|
1275
|
-
return primitiveNames.has(name) ? name : null;
|
|
1276
|
-
}
|
|
1277
|
-
const blackAlpha = /^blackAlpha\.(.+)$/.exec(r);
|
|
1278
|
-
if (blackAlpha) {
|
|
1279
|
-
const name = `BlackAlpha / ${blackAlpha[1]}`;
|
|
1280
|
-
return primitiveNames.has(name) ? name : null;
|
|
1281
|
-
}
|
|
1282
|
-
return null;
|
|
1283
|
-
}
|
|
1284
|
-
function referenceToScaleName(ref) {
|
|
1285
|
-
if (!ref || typeof ref !== "string") return null;
|
|
1286
|
-
const r = ref.trim();
|
|
1287
|
-
const scaleStep = /^([a-zA-Z]+)\.(\d+|[a-z]+)$/.exec(r);
|
|
1288
|
-
if (scaleStep) return refScaleToFigmaDisplayName(scaleStep[1]);
|
|
1289
|
-
const radixMatch = /^radix\.([a-zA-Z]+)\.(\d+)$/.exec(r);
|
|
1290
|
-
if (radixMatch) return refScaleToFigmaDisplayName(radixMatch[1]);
|
|
1291
|
-
return null;
|
|
1292
|
-
}
|
|
1293
|
-
function getReferencedScaleNames(storedColors) {
|
|
1294
|
-
const names = /* @__PURE__ */ new Set();
|
|
1295
|
-
const groupOrder = getColorGroupOrder(storedColors);
|
|
1296
|
-
for (const groupName of groupOrder) {
|
|
1297
|
-
const group = storedColors[groupName];
|
|
1298
|
-
if (!group || typeof group !== "object") continue;
|
|
1299
|
-
const keys = Object.keys(group).filter((k) => !k.startsWith("_") && k !== "governance");
|
|
1300
|
-
for (const key of keys) {
|
|
1301
|
-
const value = group[key];
|
|
1302
|
-
if (value === void 0 || typeof value !== "object" || value === null) continue;
|
|
1303
|
-
const theme = value;
|
|
1304
|
-
const lightScale = theme.light?.reference ? referenceToScaleName(theme.light.reference) : null;
|
|
1305
|
-
const darkScale = theme.dark?.reference ? referenceToScaleName(theme.dark.reference) : null;
|
|
1306
|
-
if (lightScale) names.add(lightScale);
|
|
1307
|
-
if (darkScale) names.add(darkScale);
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
return names;
|
|
1311
|
-
}
|
|
1312
|
-
function getFullScaleFromStored(storedColors, scaleName) {
|
|
1313
|
-
const key = scaleName.toLowerCase();
|
|
1314
|
-
if (key === "neutral" || key === "gray") {
|
|
1315
|
-
const neutral = storedColors.neutral;
|
|
1316
|
-
if (!neutral?.steps || typeof neutral.steps !== "object") return null;
|
|
1317
|
-
const out = {};
|
|
1318
|
-
for (const [step, stepValue] of Object.entries(neutral.steps)) {
|
|
1319
|
-
const hex = stepValue?.hex;
|
|
1320
|
-
if (typeof hex === "string" && /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/i.test(hex)) {
|
|
1321
|
-
out[step] = hex.startsWith("#") ? hex : `#${hex}`;
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
return Object.keys(out).length > 0 ? out : null;
|
|
1325
|
-
}
|
|
1326
|
-
const customScales = storedColors.customScales;
|
|
1327
|
-
if (Array.isArray(customScales)) {
|
|
1328
|
-
const keyNorm = key.replace(/\s+/g, "");
|
|
1329
|
-
const scale = customScales.find(
|
|
1330
|
-
(s) => s.name?.toLowerCase() === key || s.name?.toLowerCase().replace(/\s+/g, "") === keyNorm
|
|
1331
|
-
);
|
|
1332
|
-
if (scale?.steps && typeof scale.steps === "object") {
|
|
1333
|
-
const out = {};
|
|
1334
|
-
for (const [step, stepValue] of Object.entries(scale.steps)) {
|
|
1335
|
-
const hex = stepValue?.hex;
|
|
1336
|
-
if (typeof hex === "string" && /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/i.test(hex)) {
|
|
1337
|
-
out[step] = hex.startsWith("#") ? hex : `#${hex}`;
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
if (Object.keys(out).length > 0) return out;
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
return null;
|
|
1344
|
-
}
|
|
1345
|
-
function buildSemanticRefMap(storedColors, primitiveNames) {
|
|
1346
|
-
const out = {};
|
|
1347
|
-
const groupOrder = getColorGroupOrder(storedColors);
|
|
1348
|
-
for (const groupName of groupOrder) {
|
|
1349
|
-
const group = storedColors[groupName];
|
|
1350
|
-
if (!group || typeof group !== "object") continue;
|
|
1351
|
-
const prefix = getGroupPrefix(storedColors, groupName);
|
|
1352
|
-
const rowOrder = Array.isArray(group._rowOrder) ? group._rowOrder : void 0;
|
|
1353
|
-
const keys = Array.isArray(rowOrder) ? rowOrder : Object.keys(group).filter((k) => !k.startsWith("_") && k !== "governance");
|
|
1354
|
-
const toKebab = prefix === "action" || prefix === "brand" || prefix === "feedback";
|
|
1355
|
-
for (const key of keys) {
|
|
1356
|
-
const value = group[key];
|
|
1357
|
-
if (value === void 0 || typeof value !== "object" || value === null) continue;
|
|
1358
|
-
const theme = value;
|
|
1359
|
-
let cssKey = key === "app" ? "page" : key;
|
|
1360
|
-
if (toKebab) {
|
|
1361
|
-
cssKey = String(key).replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1362
|
-
}
|
|
1363
|
-
const fullKey = `${prefix}-${cssKey}`;
|
|
1364
|
-
const lightRef = theme.light?.reference ? referenceToFigmaPrimitiveName(theme.light.reference, primitiveNames) : null;
|
|
1365
|
-
const darkRef = theme.dark?.reference ? referenceToFigmaPrimitiveName(theme.dark.reference, primitiveNames) : null;
|
|
1366
|
-
if (lightRef || darkRef) {
|
|
1367
|
-
out[fullKey] = {};
|
|
1368
|
-
if (lightRef) out[fullKey].Light = lightRef;
|
|
1369
|
-
if (darkRef) out[fullKey].Dark = darkRef;
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
return out;
|
|
1374
|
-
}
|
|
1375
|
-
function figmaColorNameWithGroup(key) {
|
|
1376
|
-
if (key.includes("/")) {
|
|
1377
|
-
const [group2, ...rest] = key.split("/");
|
|
1378
|
-
const name2 = rest.join("/").trim();
|
|
1379
|
-
if (!name2) return key;
|
|
1380
|
-
const groupDisplay2 = group2.charAt(0).toUpperCase() + group2.slice(1).toLowerCase();
|
|
1381
|
-
return `${groupDisplay2} / ${name2}`;
|
|
1382
|
-
}
|
|
1383
|
-
const firstDash = key.indexOf("-");
|
|
1384
|
-
if (firstDash <= 0) return key;
|
|
1385
|
-
const group = key.slice(0, firstDash);
|
|
1386
|
-
const name = key.slice(firstDash + 1);
|
|
1387
|
-
const groupDisplay = group.charAt(0).toUpperCase() + group.slice(1).toLowerCase();
|
|
1388
|
-
return `${groupDisplay} / ${name}`;
|
|
1389
|
-
}
|
|
1390
|
-
var FIGMA_SHADOW_ORDER = {
|
|
1391
|
-
none: 0,
|
|
1392
|
-
xs: 1,
|
|
1393
|
-
sm: 2,
|
|
1394
|
-
md: 3,
|
|
1395
|
-
lg: 4,
|
|
1396
|
-
xl: 5,
|
|
1397
|
-
"2xl": 6,
|
|
1398
|
-
focus: 7
|
|
1399
|
-
};
|
|
1400
|
-
function tokenValueToNumber(s) {
|
|
1401
|
-
if (typeof s !== "string" || !s.trim()) return 0;
|
|
1402
|
-
const t = s.trim();
|
|
1403
|
-
if (t.endsWith("rem")) {
|
|
1404
|
-
const n2 = parseFloat(t.replace(/rem$/, ""));
|
|
1405
|
-
return Number.isFinite(n2) ? Math.round(n2 * 16) : 0;
|
|
1406
|
-
}
|
|
1407
|
-
if (t.endsWith("px")) {
|
|
1408
|
-
const n2 = parseFloat(t.replace(/px$/, ""));
|
|
1409
|
-
return Number.isFinite(n2) ? Math.round(n2) : 0;
|
|
1410
|
-
}
|
|
1411
|
-
const n = parseFloat(t);
|
|
1412
|
-
return Number.isFinite(n) ? n : 0;
|
|
1413
|
-
}
|
|
1414
|
-
function parseBoxShadowToFigmaEffect(shadowStr) {
|
|
1415
|
-
const s = shadowStr.trim();
|
|
1416
|
-
if (!s || s.toLowerCase() === "none") return null;
|
|
1417
|
-
const parsePx = (x) => typeof x === "string" ? parseFloat(x.replace(/px$/i, "")) : NaN;
|
|
1418
|
-
const colorMatch = s.match(/(rgba?\s*\([^)]+\)|#[0-9A-Fa-f]{3,8})\s*$/i);
|
|
1419
|
-
const colorStr = colorMatch ? colorMatch[1].trim() : void 0;
|
|
1420
|
-
const rest = (colorMatch ? s.slice(0, colorMatch.index) : s).trim();
|
|
1421
|
-
const parts = rest ? rest.split(/\s+/) : [];
|
|
1422
|
-
if (parts.length < 3) return null;
|
|
1423
|
-
const offsetX = parsePx(parts[0]);
|
|
1424
|
-
const offsetY = parsePx(parts[1]);
|
|
1425
|
-
const blur = parsePx(parts[2]);
|
|
1426
|
-
let spread = 0;
|
|
1427
|
-
if (parts.length >= 4) spread = parsePx(parts[3]);
|
|
1428
|
-
let r = 0, g = 0, b = 0, a = 0.1;
|
|
1429
|
-
if (colorStr) {
|
|
1430
|
-
const rgbaMatch = colorStr.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i);
|
|
1431
|
-
if (rgbaMatch) {
|
|
1432
|
-
r = Number(rgbaMatch[1]) / 255;
|
|
1433
|
-
g = Number(rgbaMatch[2]) / 255;
|
|
1434
|
-
b = Number(rgbaMatch[3]) / 255;
|
|
1435
|
-
a = rgbaMatch[4] != null ? parseFloat(rgbaMatch[4]) : 1;
|
|
1436
|
-
} else {
|
|
1437
|
-
const hexMatch = colorStr.match(/^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/);
|
|
1438
|
-
if (hexMatch) {
|
|
1439
|
-
r = parseInt(hexMatch[1].slice(0, 2), 16) / 255;
|
|
1440
|
-
g = parseInt(hexMatch[1].slice(2, 4), 16) / 255;
|
|
1441
|
-
b = parseInt(hexMatch[1].slice(4, 6), 16) / 255;
|
|
1442
|
-
a = hexMatch[2] ? parseInt(hexMatch[2], 16) / 255 : 0.1;
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
if (!Number.isFinite(offsetX) || !Number.isFinite(offsetY) || !Number.isFinite(blur)) return null;
|
|
1447
|
-
return {
|
|
1448
|
-
type: "DROP_SHADOW",
|
|
1449
|
-
offset: { x: offsetX, y: offsetY },
|
|
1450
|
-
radius: Math.max(0, blur),
|
|
1451
|
-
spread: Number.isFinite(spread) ? spread : 0,
|
|
1452
|
-
color: { r, g, b, a },
|
|
1453
|
-
visible: true,
|
|
1454
|
-
blendMode: "NORMAL"
|
|
1455
|
-
};
|
|
1456
|
-
}
|
|
1457
|
-
function parseBoxShadowToFigmaEffects(shadowStr) {
|
|
1458
|
-
const s = (shadowStr || "").trim();
|
|
1459
|
-
if (!s || s.toLowerCase() === "none") return [];
|
|
1460
|
-
const out = [];
|
|
1461
|
-
const segments = s.split(/\s*,\s*/);
|
|
1462
|
-
for (const seg of segments) {
|
|
1463
|
-
const effect = parseBoxShadowToFigmaEffect(seg.trim());
|
|
1464
|
-
if (effect) out.push(effect);
|
|
1465
|
-
}
|
|
1466
|
-
return out;
|
|
1467
|
-
}
|
|
1468
|
-
function colorTo8DigitHex(color) {
|
|
1469
|
-
if (!color || typeof color !== "string") return null;
|
|
1470
|
-
const s = color.trim();
|
|
1471
|
-
const rgbaMatch = s.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i);
|
|
1472
|
-
if (rgbaMatch) {
|
|
1473
|
-
const r = Math.max(0, Math.min(255, parseInt(rgbaMatch[1], 10)));
|
|
1474
|
-
const g = Math.max(0, Math.min(255, parseInt(rgbaMatch[2], 10)));
|
|
1475
|
-
const b = Math.max(0, Math.min(255, parseInt(rgbaMatch[3], 10)));
|
|
1476
|
-
const a = rgbaMatch[4] != null ? Math.max(0, Math.min(1, parseFloat(rgbaMatch[4]))) : 1;
|
|
1477
|
-
const aByte = Math.round(a * 255);
|
|
1478
|
-
const hex = "#" + [r, g, b, aByte].map((n) => n.toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
1479
|
-
return hex;
|
|
1480
|
-
}
|
|
1481
|
-
const hexMatch = s.match(/^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/i);
|
|
1482
|
-
if (hexMatch) {
|
|
1483
|
-
const rgb = hexMatch[1];
|
|
1484
|
-
const aa = hexMatch[2] ?? "FF";
|
|
1485
|
-
return `#${rgb}${aa}`.toUpperCase();
|
|
1486
|
-
}
|
|
1487
|
-
return null;
|
|
1488
|
-
}
|
|
1489
|
-
function typesetKeyToFontFamilyRole(key) {
|
|
1490
|
-
const prefix = (key.split("-")[0] ?? key).toLowerCase();
|
|
1491
|
-
if (prefix === "display" || prefix.startsWith("display")) return "display";
|
|
1492
|
-
if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
|
|
1493
|
-
if (prefix === "mono" || prefix.startsWith("mono")) return "mono";
|
|
1494
|
-
if (prefix.startsWith("body")) return "body";
|
|
1495
|
-
return "body";
|
|
1496
|
-
}
|
|
1497
|
-
function buildFigmaPayloadsFromDS(data) {
|
|
1498
|
-
const tokens = data.tokens;
|
|
1499
|
-
const colors = tokens?.colors;
|
|
1500
|
-
const typography = tokens?.typography;
|
|
1501
|
-
const hexRe = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
|
|
1502
|
-
const modes = [];
|
|
1503
|
-
if (colors?.modes) {
|
|
1504
|
-
const light = colors.modes.light ?? {};
|
|
1505
|
-
const dark = colors.modes.dark ?? {};
|
|
1506
|
-
if (Object.keys(light).length > 0) modes.push("Light");
|
|
1507
|
-
if (Object.keys(dark).length > 0 && !modes.includes("Dark")) modes.push("Dark");
|
|
1508
|
-
}
|
|
1509
|
-
if (modes.length === 0) modes.push("Light");
|
|
1510
|
-
const primitiveModes = ["Value"];
|
|
1511
|
-
const dsName = data.meta?.name;
|
|
1512
|
-
const collectionPrefix = dsName ? `${dsName} ` : "";
|
|
1513
|
-
const referencedScaleNames = data.storedColors ? getReferencedScaleNames(data.storedColors) : /* @__PURE__ */ new Set();
|
|
1514
|
-
const primitiveVariables = [];
|
|
1515
|
-
const primitiveNames = /* @__PURE__ */ new Set();
|
|
1516
|
-
const oneOffHexToFigmaName = /* @__PURE__ */ new Map();
|
|
1517
|
-
const alphaScales = colors?.alphaScales;
|
|
1518
|
-
if (alphaScales?.whiteAlpha && typeof alphaScales.whiteAlpha === "object") {
|
|
1519
|
-
for (const [step, value] of Object.entries(alphaScales.whiteAlpha)) {
|
|
1520
|
-
if (typeof value !== "string") continue;
|
|
1521
|
-
const hex = colorTo8DigitHex(value);
|
|
1522
|
-
if (!hex || !hexRe.test(hex)) continue;
|
|
1523
|
-
const figmaName = `WhiteAlpha / ${step}`;
|
|
1524
|
-
if (primitiveNames.has(figmaName)) continue;
|
|
1525
|
-
primitiveNames.add(figmaName);
|
|
1526
|
-
const values = {};
|
|
1527
|
-
for (const m of primitiveModes) values[m] = hex;
|
|
1528
|
-
primitiveVariables.push({ name: figmaName, values });
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
if (alphaScales?.blackAlpha && typeof alphaScales.blackAlpha === "object") {
|
|
1532
|
-
for (const [step, value] of Object.entries(alphaScales.blackAlpha)) {
|
|
1533
|
-
if (typeof value !== "string") continue;
|
|
1534
|
-
const hex = colorTo8DigitHex(value);
|
|
1535
|
-
if (!hex || !hexRe.test(hex)) continue;
|
|
1536
|
-
const figmaName = `BlackAlpha / ${step}`;
|
|
1537
|
-
if (primitiveNames.has(figmaName)) continue;
|
|
1538
|
-
primitiveNames.add(figmaName);
|
|
1539
|
-
const values = {};
|
|
1540
|
-
for (const m of primitiveModes) values[m] = hex;
|
|
1541
|
-
primitiveVariables.push({ name: figmaName, values });
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
if (colors?.scales && typeof colors.scales === "object") {
|
|
1545
|
-
for (const [scaleName, steps] of Object.entries(colors.scales)) {
|
|
1546
|
-
if (!steps || typeof steps !== "object") continue;
|
|
1547
|
-
let groupDisplay = scaleName.charAt(0).toUpperCase() + scaleName.slice(1);
|
|
1548
|
-
if (scaleName.toLowerCase() === "neutral" && data.storedColors) {
|
|
1549
|
-
const neutral = data.storedColors.neutral;
|
|
1550
|
-
if (neutral?.baseHueFamily) {
|
|
1551
|
-
groupDisplay = refScaleToFigmaDisplayName(neutral.baseHueFamily);
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
for (const [step, hexVal] of Object.entries(steps)) {
|
|
1555
|
-
if (typeof hexVal !== "string") continue;
|
|
1556
|
-
const hex = colorTo8DigitHex(hexVal) ?? (hexRe.test(hexVal) ? hexVal : null);
|
|
1557
|
-
if (!hex || !hexRe.test(hex)) continue;
|
|
1558
|
-
const figmaName = `${groupDisplay} / ${step}`;
|
|
1559
|
-
if (primitiveNames.has(figmaName)) continue;
|
|
1560
|
-
primitiveNames.add(figmaName);
|
|
1561
|
-
const values = {};
|
|
1562
|
-
for (const m of primitiveModes) values[m] = hex;
|
|
1563
|
-
primitiveVariables.push({ name: figmaName, values });
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
const radixVariables = [];
|
|
1568
|
-
const radixCollectionName = `${collectionPrefix}Colors Radix`;
|
|
1569
|
-
for (const scaleName of referencedScaleNames) {
|
|
1570
|
-
if (primitiveNames.has(`${scaleName} / 50`) || primitiveNames.has(`${scaleName} / 100`)) continue;
|
|
1571
|
-
const fromStored = data.storedColors ? getFullScaleFromStored(data.storedColors, scaleName) : null;
|
|
1572
|
-
const scaleData = fromStored ?? data.getRadixScale?.(scaleName) ?? null;
|
|
1573
|
-
if (!scaleData) continue;
|
|
1574
|
-
for (const [step, hexVal] of Object.entries(scaleData)) {
|
|
1575
|
-
const figmaName = `${scaleName} / ${step}`;
|
|
1576
|
-
if (primitiveNames.has(figmaName)) continue;
|
|
1577
|
-
const hex = colorTo8DigitHex(hexVal) ?? (hexRe.test(hexVal) ? hexVal : null);
|
|
1578
|
-
if (!hex || !hexRe.test(hex)) continue;
|
|
1579
|
-
primitiveNames.add(figmaName);
|
|
1580
|
-
const values = {};
|
|
1581
|
-
for (const m of primitiveModes) values[m] = hex;
|
|
1582
|
-
primitiveVariables.push({ name: figmaName, values });
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
if (colors?.oneOffs && Array.isArray(colors.oneOffs)) {
|
|
1586
|
-
for (const oneOff of colors.oneOffs) {
|
|
1587
|
-
if (!oneOff || typeof oneOff !== "object" || typeof oneOff.hex !== "string") continue;
|
|
1588
|
-
const hex = colorTo8DigitHex(oneOff.hex) ?? (hexRe.test(oneOff.hex) ? oneOff.hex : null);
|
|
1589
|
-
if (!hex || !hexRe.test(hex)) continue;
|
|
1590
|
-
const name = typeof oneOff.name === "string" && oneOff.name ? oneOff.name : oneOff.id ?? "unnamed";
|
|
1591
|
-
const figmaName = `One-off / ${name}`;
|
|
1592
|
-
if (primitiveNames.has(figmaName)) continue;
|
|
1593
|
-
primitiveNames.add(figmaName);
|
|
1594
|
-
const values = {};
|
|
1595
|
-
for (const m of primitiveModes) values[m] = hex;
|
|
1596
|
-
primitiveVariables.push({ name: figmaName, values });
|
|
1597
|
-
const normalizedHex = hex.replace(/^#/, "").toUpperCase();
|
|
1598
|
-
const key8 = normalizedHex.length === 6 ? normalizedHex + "FF" : normalizedHex;
|
|
1599
|
-
oneOffHexToFigmaName.set(key8, figmaName);
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
const semanticRefMap = data.storedColors && primitiveNames.size > 0 ? buildSemanticRefMap(data.storedColors, primitiveNames) : {};
|
|
1603
|
-
const semanticVariables = [];
|
|
1604
|
-
const semanticNames = /* @__PURE__ */ new Set();
|
|
1605
|
-
const primitivesCollectionName = `${collectionPrefix}Colors Primitives`;
|
|
1606
|
-
if (colors?.modes) {
|
|
1607
|
-
const light = colors.modes.light ?? {};
|
|
1608
|
-
const dark = colors.modes.dark ?? {};
|
|
1609
|
-
const orderedKeys = [...Object.keys(light)];
|
|
1610
|
-
for (const k of Object.keys(dark)) {
|
|
1611
|
-
if (!orderedKeys.includes(k)) orderedKeys.push(k);
|
|
1612
|
-
}
|
|
1613
|
-
for (const key of orderedKeys) {
|
|
1614
|
-
const lightVal = light[key];
|
|
1615
|
-
const darkVal = dark[key];
|
|
1616
|
-
const lightHex = typeof lightVal === "string" ? colorTo8DigitHex(lightVal) ?? (hexRe.test(lightVal) ? lightVal : null) : null;
|
|
1617
|
-
if (lightHex && hexRe.test(lightHex)) {
|
|
1618
|
-
const figmaName = figmaColorNameWithGroup(key);
|
|
1619
|
-
if (semanticNames.has(figmaName)) continue;
|
|
1620
|
-
semanticNames.add(figmaName);
|
|
1621
|
-
const darkHex = typeof darkVal === "string" ? colorTo8DigitHex(darkVal) ?? (hexRe.test(darkVal) ? darkVal : null) : null;
|
|
1622
|
-
const refs = semanticRefMap[key];
|
|
1623
|
-
const values = {
|
|
1624
|
-
...modes.includes("Light") && { Light: lightHex },
|
|
1625
|
-
...modes.includes("Dark") && { Dark: darkHex && hexRe.test(darkHex) ? darkHex : lightHex }
|
|
1626
|
-
};
|
|
1627
|
-
const aliasByMode = {};
|
|
1628
|
-
for (const m of modes) {
|
|
1629
|
-
const aliasFromRef = m === "Light" ? refs?.Light : refs?.Dark;
|
|
1630
|
-
if (aliasFromRef && primitiveNames.has(aliasFromRef)) {
|
|
1631
|
-
aliasByMode[m] = aliasFromRef;
|
|
1632
|
-
continue;
|
|
1633
|
-
}
|
|
1634
|
-
const hexForMode = m === "Light" ? lightHex : darkHex && hexRe.test(darkHex) ? darkHex : lightHex;
|
|
1635
|
-
const norm = hexForMode.replace(/^#/, "").toUpperCase();
|
|
1636
|
-
const key8 = norm.length === 6 ? norm + "FF" : norm;
|
|
1637
|
-
const oneOffAlias = oneOffHexToFigmaName.get(key8);
|
|
1638
|
-
if (oneOffAlias) {
|
|
1639
|
-
aliasByMode[m] = oneOffAlias;
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
semanticVariables.push({
|
|
1643
|
-
name: figmaName,
|
|
1644
|
-
values,
|
|
1645
|
-
...Object.keys(aliasByMode).length > 0 ? { aliasByMode } : {}
|
|
1646
|
-
});
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
if (colors?.static?.brand && typeof colors.static.brand === "object") {
|
|
1651
|
-
for (const [key, hex] of Object.entries(colors.static.brand)) {
|
|
1652
|
-
if (typeof hex !== "string" || !hexRe.test(hex)) continue;
|
|
1653
|
-
const figmaName = figmaColorNameWithGroup(`brand/${key}`);
|
|
1654
|
-
if (semanticNames.has(figmaName)) continue;
|
|
1655
|
-
semanticNames.add(figmaName);
|
|
1656
|
-
const values = {};
|
|
1657
|
-
for (const m of modes) values[m] = hex;
|
|
1658
|
-
semanticVariables.push({ name: figmaName, values });
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
const colorVariableCollections = [];
|
|
1662
|
-
if (primitiveVariables.length > 0) {
|
|
1663
|
-
colorVariableCollections.push({
|
|
1664
|
-
collectionName: primitivesCollectionName,
|
|
1665
|
-
modes: primitiveModes,
|
|
1666
|
-
variables: primitiveVariables,
|
|
1667
|
-
applyScopes: false
|
|
1668
|
-
});
|
|
1669
|
-
}
|
|
1670
|
-
if (radixVariables.length > 0) {
|
|
1671
|
-
colorVariableCollections.push({
|
|
1672
|
-
collectionName: radixCollectionName,
|
|
1673
|
-
modes,
|
|
1674
|
-
variables: radixVariables,
|
|
1675
|
-
applyScopes: false
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
if (semanticVariables.length > 0) {
|
|
1679
|
-
const primitiveCollections = [];
|
|
1680
|
-
if (primitiveVariables.length > 0) primitiveCollections.push(primitivesCollectionName);
|
|
1681
|
-
if (radixVariables.length > 0) primitiveCollections.push(radixCollectionName);
|
|
1682
|
-
colorVariableCollections.push({
|
|
1683
|
-
collectionName: `${collectionPrefix}Colors Semantic`,
|
|
1684
|
-
modes,
|
|
1685
|
-
variables: semanticVariables,
|
|
1686
|
-
applyScopes: true,
|
|
1687
|
-
...primitiveCollections.length > 1 ? { primitiveCollectionNames: primitiveCollections } : primitiveCollections.length === 1 ? { primitiveCollectionName: primitiveCollections[0] } : {}
|
|
1688
|
-
});
|
|
1689
|
-
}
|
|
1690
|
-
const paintStyles = [];
|
|
1691
|
-
const textStyles = [];
|
|
1692
|
-
const sizeToPx = (val, basePx = 16) => {
|
|
1693
|
-
if (typeof val === "number") return Math.round(val);
|
|
1694
|
-
const s = String(val).trim();
|
|
1695
|
-
const pxMatch = s.match(/^([\d.]+)\s*px$/i);
|
|
1696
|
-
if (pxMatch) return Math.round(parseFloat(pxMatch[1]));
|
|
1697
|
-
const remMatch = s.match(/^([\d.]+)\s*rem$/i);
|
|
1698
|
-
if (remMatch) return Math.round(parseFloat(remMatch[1]) * basePx);
|
|
1699
|
-
const n = parseFloat(s);
|
|
1700
|
-
if (Number.isFinite(n)) return n <= 0 ? basePx : n < 50 ? Math.round(n * basePx) : Math.round(n);
|
|
1701
|
-
return basePx;
|
|
1702
|
-
};
|
|
1703
|
-
const letterSpacingToPx = (val, fontSizePx) => {
|
|
1704
|
-
if (val === void 0 || val === null) return void 0;
|
|
1705
|
-
if (typeof val === "number") return Math.round(val);
|
|
1706
|
-
const s = String(val).trim();
|
|
1707
|
-
const pxMatch = s.match(/^([-\d.]+)\s*px$/i);
|
|
1708
|
-
if (pxMatch) return Math.round(parseFloat(pxMatch[1]));
|
|
1709
|
-
const emMatch = s.match(/^([-\d.]+)\s*em$/i);
|
|
1710
|
-
if (emMatch) return Math.round(parseFloat(emMatch[1]) * fontSizePx);
|
|
1711
|
-
const n = parseFloat(s);
|
|
1712
|
-
return Number.isFinite(n) ? Math.round(n) : void 0;
|
|
1713
|
-
};
|
|
1714
|
-
const firstFont = (obj) => {
|
|
1715
|
-
if (typeof obj === "string") {
|
|
1716
|
-
const primary = obj.split(",")[0].trim().replace(/^['"]|['"]$/g, "");
|
|
1717
|
-
return primary || "Inter";
|
|
1718
|
-
}
|
|
1719
|
-
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
1720
|
-
const v = obj.body ?? obj.heading ?? obj.display ?? Object.values(obj)[0];
|
|
1721
|
-
return firstFont(v);
|
|
1722
|
-
}
|
|
1723
|
-
return "Inter";
|
|
1724
|
-
};
|
|
1725
|
-
const toFontFamilyString = (val) => {
|
|
1726
|
-
if (typeof val === "string") {
|
|
1727
|
-
const s = val.trim().replace(/^["']|["']$/g, "");
|
|
1728
|
-
return s || "Inter";
|
|
1729
|
-
}
|
|
1730
|
-
return firstFont(val);
|
|
1731
|
-
};
|
|
1732
|
-
const fontFamilyMap = typography?.fontFamily ?? {};
|
|
1733
|
-
const defaultFontFamily = typography ? firstFont(typography.fontFamily ?? "Inter") : "Inter";
|
|
1734
|
-
const fontSizeMap = typography?.fontSize;
|
|
1735
|
-
const fontWeightMap = typography?.fontWeight;
|
|
1736
|
-
const lineHeightMap = typography?.lineHeight;
|
|
1737
|
-
const letterSpacingMap = typography?.letterSpacing;
|
|
1738
|
-
const textTransformMap = typography?.textTransform;
|
|
1739
|
-
const textDecorationMap = typography?.textDecoration;
|
|
1740
|
-
if (fontSizeMap && typeof fontSizeMap === "object" && Object.keys(fontSizeMap).length > 0) {
|
|
1741
|
-
for (const [key, sizeVal] of Object.entries(fontSizeMap)) {
|
|
1742
|
-
const fontSize = sizeToPx(sizeVal);
|
|
1743
|
-
if (fontSize <= 0) continue;
|
|
1744
|
-
const role = typesetKeyToFontFamilyRole(key);
|
|
1745
|
-
const fontFamily = toFontFamilyString(
|
|
1746
|
-
fontFamilyMap[role] ?? fontFamilyMap.body ?? fontFamilyMap.heading ?? fontFamilyMap.display ?? defaultFontFamily
|
|
1747
|
-
);
|
|
1748
|
-
const lh = lineHeightMap && typeof lineHeightMap === "object" ? lineHeightMap[key] : void 0;
|
|
1749
|
-
const weight = fontWeightMap && typeof fontWeightMap === "object" ? fontWeightMap[key] : void 0;
|
|
1750
|
-
const fontWeight = weight != null ? String(weight) : "400";
|
|
1751
|
-
const letterSpacingPx = letterSpacingToPx(
|
|
1752
|
-
letterSpacingMap && typeof letterSpacingMap === "object" ? letterSpacingMap[key] : void 0,
|
|
1753
|
-
fontSize
|
|
1754
|
-
);
|
|
1755
|
-
const textTransform = textTransformMap && typeof textTransformMap === "object" ? textTransformMap[key] : void 0;
|
|
1756
|
-
const textDecoration = textDecorationMap && typeof textDecorationMap === "object" ? textDecorationMap[key] : void 0;
|
|
1757
|
-
const namePart = key.replace(/-/g, " / ");
|
|
1758
|
-
const style = {
|
|
1759
|
-
name: namePart.startsWith("Typography") ? namePart : `Typography / ${namePart}`,
|
|
1760
|
-
fontFamily,
|
|
1761
|
-
fontWeight,
|
|
1762
|
-
fontSize,
|
|
1763
|
-
lineHeightUnit: "PERCENT",
|
|
1764
|
-
letterSpacingUnit: "PIXELS",
|
|
1765
|
-
...letterSpacingPx !== void 0 && letterSpacingPx !== 0 ? { letterSpacingValue: letterSpacingPx } : {}
|
|
1766
|
-
};
|
|
1767
|
-
if (lh != null && typeof lh === "number" && lh > 0) {
|
|
1768
|
-
style.lineHeightValue = lh >= 10 ? Math.round(lh / fontSize * 100) : Math.round(lh * 100);
|
|
1769
|
-
} else {
|
|
1770
|
-
style.lineHeightValue = 150;
|
|
1771
|
-
}
|
|
1772
|
-
if (textTransform === "uppercase") style.textCase = "UPPER";
|
|
1773
|
-
else if (textTransform === "lowercase") style.textCase = "LOWER";
|
|
1774
|
-
else if (textTransform === "capitalize") style.textCase = "TITLE";
|
|
1775
|
-
else style.textCase = "ORIGINAL";
|
|
1776
|
-
if (textDecoration === "underline") style.textDecoration = "UNDERLINE";
|
|
1777
|
-
else style.textDecoration = "NONE";
|
|
1778
|
-
textStyles.push(style);
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
const textStylesMap = typography?.textStyles;
|
|
1782
|
-
if (textStyles.length === 0 && textStylesMap && typeof textStylesMap === "object") {
|
|
1783
|
-
for (const [styleName, style] of Object.entries(textStylesMap)) {
|
|
1784
|
-
if (!style || typeof style !== "object") continue;
|
|
1785
|
-
const fontSize = sizeToPx(style.fontSize ?? "1rem");
|
|
1786
|
-
const lhStr = style.lineHeight;
|
|
1787
|
-
const lineHeightUnitless = lhStr != null ? lhStr.endsWith("%") ? parseFloat(lhStr) / 100 : sizeToPx(lhStr) / fontSize : 1.5;
|
|
1788
|
-
const payload = {
|
|
1789
|
-
name: styleName.startsWith("Typography") ? styleName : `Typography / ${styleName.replace(/\//g, " / ")}`,
|
|
1790
|
-
fontFamily: defaultFontFamily,
|
|
1791
|
-
fontWeight: String(style.fontWeight ?? "400"),
|
|
1792
|
-
fontSize,
|
|
1793
|
-
lineHeightUnit: "PERCENT",
|
|
1794
|
-
lineHeightValue: Math.round((Number.isFinite(lineHeightUnitless) ? lineHeightUnitless : 1.5) * 100),
|
|
1795
|
-
letterSpacingUnit: "PIXELS",
|
|
1796
|
-
textCase: "ORIGINAL",
|
|
1797
|
-
textDecoration: "NONE"
|
|
1798
|
-
};
|
|
1799
|
-
textStyles.push(payload);
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
textStyles.sort((a, b) => {
|
|
1803
|
-
if (a.fontSize !== b.fontSize) return a.fontSize - b.fontSize;
|
|
1804
|
-
return (a.name || "").localeCompare(b.name || "");
|
|
1805
|
-
});
|
|
1806
|
-
const numberVariableCollections = [];
|
|
1807
|
-
const spacing = tokens?.spacing;
|
|
1808
|
-
if (spacing?.scale && typeof spacing.scale === "object") {
|
|
1809
|
-
const vars = [];
|
|
1810
|
-
for (const [key, val] of Object.entries(spacing.scale)) {
|
|
1811
|
-
const n = tokenValueToNumber(val);
|
|
1812
|
-
if (n >= 0) vars.push({ name: `Spacing / ${key}`, value: n });
|
|
1813
|
-
}
|
|
1814
|
-
vars.sort((a, b) => a.value - b.value);
|
|
1815
|
-
if (vars.length > 0)
|
|
1816
|
-
numberVariableCollections.push({
|
|
1817
|
-
collectionName: `${collectionPrefix}Spacing`,
|
|
1818
|
-
categoryKey: "Spacing",
|
|
1819
|
-
variables: vars,
|
|
1820
|
-
scopes: ["GAP"]
|
|
1821
|
-
});
|
|
1822
|
-
}
|
|
1823
|
-
const radius = tokens?.radius;
|
|
1824
|
-
if (radius?.scale && typeof radius.scale === "object") {
|
|
1825
|
-
const vars = [];
|
|
1826
|
-
for (const [key, val] of Object.entries(radius.scale)) {
|
|
1827
|
-
const n = tokenValueToNumber(val);
|
|
1828
|
-
if (n >= 0) vars.push({ name: `Radius / ${key}`, value: n });
|
|
1829
|
-
}
|
|
1830
|
-
vars.sort((a, b) => a.value - b.value);
|
|
1831
|
-
if (vars.length > 0)
|
|
1832
|
-
numberVariableCollections.push({
|
|
1833
|
-
collectionName: `${collectionPrefix}Radius`,
|
|
1834
|
-
categoryKey: "Radius",
|
|
1835
|
-
variables: vars,
|
|
1836
|
-
scopes: ["CORNER_RADIUS"]
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
|
-
const borders = tokens?.borders;
|
|
1840
|
-
if (borders?.width && typeof borders.width === "object") {
|
|
1841
|
-
const vars = [];
|
|
1842
|
-
for (const [key, val] of Object.entries(borders.width)) {
|
|
1843
|
-
const n = tokenValueToNumber(val);
|
|
1844
|
-
if (n >= 0) vars.push({ name: `Borders / ${key}`, value: n });
|
|
1845
|
-
}
|
|
1846
|
-
vars.sort((a, b) => a.value - b.value);
|
|
1847
|
-
if (vars.length > 0)
|
|
1848
|
-
numberVariableCollections.push({
|
|
1849
|
-
collectionName: `${collectionPrefix}Borders`,
|
|
1850
|
-
categoryKey: "Borders",
|
|
1851
|
-
variables: vars,
|
|
1852
|
-
scopes: ["STROKE_FLOAT"]
|
|
1853
|
-
});
|
|
1854
|
-
}
|
|
1855
|
-
const sizing = tokens?.sizing;
|
|
1856
|
-
const sizingVariables = [];
|
|
1857
|
-
if (sizing?.height && typeof sizing.height === "object") {
|
|
1858
|
-
for (const [key, val] of Object.entries(sizing.height)) {
|
|
1859
|
-
const n = tokenValueToNumber(val);
|
|
1860
|
-
if (n >= 0) sizingVariables.push({ name: `Height / ${key}`, value: n });
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
if (sizing?.icon && typeof sizing.icon === "object") {
|
|
1864
|
-
for (const [key, val] of Object.entries(sizing.icon)) {
|
|
1865
|
-
const n = tokenValueToNumber(val);
|
|
1866
|
-
if (n >= 0) sizingVariables.push({ name: `Icon / ${key}`, value: n });
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
sizingVariables.sort((a, b) => a.value - b.value);
|
|
1870
|
-
if (sizingVariables.length > 0) {
|
|
1871
|
-
numberVariableCollections.push({
|
|
1872
|
-
collectionName: `${collectionPrefix}Sizing`,
|
|
1873
|
-
categoryKey: "Sizing",
|
|
1874
|
-
variables: sizingVariables,
|
|
1875
|
-
scopes: ["WIDTH_HEIGHT"]
|
|
1876
|
-
});
|
|
1877
|
-
}
|
|
1878
|
-
const layout = tokens?.layout;
|
|
1879
|
-
if (layout?.breakpoint && typeof layout.breakpoint === "object") {
|
|
1880
|
-
const vars = [];
|
|
1881
|
-
for (const [key, val] of Object.entries(layout.breakpoint)) {
|
|
1882
|
-
const n = tokenValueToNumber(val);
|
|
1883
|
-
if (n >= 0) vars.push({ name: `Breakpoint / ${key}`, value: n });
|
|
1884
|
-
}
|
|
1885
|
-
vars.sort((a, b) => a.value - b.value);
|
|
1886
|
-
if (vars.length > 0)
|
|
1887
|
-
numberVariableCollections.push({
|
|
1888
|
-
collectionName: `${collectionPrefix}Layout`,
|
|
1889
|
-
categoryKey: "Layout",
|
|
1890
|
-
variables: vars,
|
|
1891
|
-
scopes: ["WIDTH_HEIGHT"]
|
|
1892
|
-
});
|
|
1893
|
-
}
|
|
1894
|
-
const effectStyles = [];
|
|
1895
|
-
const shadows = tokens?.shadows;
|
|
1896
|
-
if (shadows?.elevation && typeof shadows.elevation === "object") {
|
|
1897
|
-
for (const [key, val] of Object.entries(shadows.elevation)) {
|
|
1898
|
-
if (typeof val !== "string") continue;
|
|
1899
|
-
const effects = parseBoxShadowToFigmaEffects(val);
|
|
1900
|
-
if (effects.length > 0) effectStyles.push({ name: `Shadow / ${key}`, effects });
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
if (shadows?.focus && typeof shadows.focus === "string") {
|
|
1904
|
-
const effects = parseBoxShadowToFigmaEffects(shadows.focus);
|
|
1905
|
-
if (effects.length > 0) effectStyles.push({ name: "Shadow / focus", effects });
|
|
1906
|
-
}
|
|
1907
|
-
effectStyles.sort((a, b) => {
|
|
1908
|
-
const nameA = a.name.startsWith("Shadow / ") ? a.name.slice(9) : a.name;
|
|
1909
|
-
const nameB = b.name.startsWith("Shadow / ") ? b.name.slice(9) : b.name;
|
|
1910
|
-
const orderA = FIGMA_SHADOW_ORDER[nameA] ?? 100;
|
|
1911
|
-
const orderB = FIGMA_SHADOW_ORDER[nameB] ?? 100;
|
|
1912
|
-
if (orderA !== orderB) return orderA - orderB;
|
|
1913
|
-
return nameA.localeCompare(nameB);
|
|
1914
|
-
});
|
|
1915
|
-
return {
|
|
1916
|
-
colorVariableCollections,
|
|
1917
|
-
paintStyles,
|
|
1918
|
-
textStyles,
|
|
1919
|
-
numberVariableCollections,
|
|
1920
|
-
effectStyles
|
|
1921
|
-
};
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
1227
|
// src/index.ts
|
|
1925
1228
|
import * as path from "path";
|
|
1926
1229
|
import * as fs from "fs";
|
|
@@ -2124,7 +1427,7 @@ function parseArgs() {
|
|
|
2124
1427
|
var cliArgs = parseArgs();
|
|
2125
1428
|
var { dsId, apiKey, accessToken } = cliArgs;
|
|
2126
1429
|
var apiBase = cliArgs.apiBase || "https://atomix.studio";
|
|
2127
|
-
var MCP_VERSION = "1.0.
|
|
1430
|
+
var MCP_VERSION = "1.0.34";
|
|
2128
1431
|
var cachedData = null;
|
|
2129
1432
|
var cachedETag = null;
|
|
2130
1433
|
var cachedMcpTier = null;
|
|
@@ -2230,7 +1533,7 @@ async function fetchDesignSystemForMCP(forceRefresh = false) {
|
|
|
2230
1533
|
return result.data;
|
|
2231
1534
|
}
|
|
2232
1535
|
var TOKEN_CATEGORIES = ["colors", "typography", "spacing", "sizing", "shadows", "radius", "borders", "motion", "zIndex"];
|
|
2233
|
-
function
|
|
1536
|
+
function typesetKeyToFontFamilyRole(key) {
|
|
2234
1537
|
const prefix = (key.split("-")[0] ?? key).toLowerCase();
|
|
2235
1538
|
if (prefix === "display" || prefix.startsWith("display")) return "display";
|
|
2236
1539
|
if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
|
|
@@ -2250,7 +1553,7 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
|
|
|
2250
1553
|
const p = cssPrefix ? `${cssPrefix}-` : "";
|
|
2251
1554
|
const typesets = [];
|
|
2252
1555
|
for (const key of Object.keys(fontSize)) {
|
|
2253
|
-
const role =
|
|
1556
|
+
const role = typesetKeyToFontFamilyRole(key);
|
|
2254
1557
|
const familyName = fontFamily[role] ?? fontFamily.body;
|
|
2255
1558
|
const fontFamilyVarName = familyName ? `--${p}typography-font-family-${role}` : void 0;
|
|
2256
1559
|
const fontFamilyVar = familyName ? `var(${fontFamilyVarName})` : "";
|
|
@@ -2439,7 +1742,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2439
1742
|
},
|
|
2440
1743
|
{
|
|
2441
1744
|
name: "syncAll",
|
|
2442
|
-
description: "Sync tokens, AI rules, skills (.cursor/skills/atomix-ds/SKILL.md), and atomix-dependencies.json. All paths are resolved under workspaceRoot so files are written inside the project repo (committable). Use dryRun: true first to report what would change without writing; then dryRun: false to apply. Optional: workspaceRoot (project root; default: ATOMIX_PROJECT_ROOT env or process.cwd()), output (default ./tokens.css), format (default css), skipTokens, dryRun.",
|
|
1745
|
+
description: "Sync tokens, AI rules, skills (.cursor/skills/atomix-ds/SKILL.md and figma-design-SKILL.md), and atomix-dependencies.json. All paths are resolved under workspaceRoot so files are written inside the project repo (committable). Use dryRun: true first to report what would change without writing; then dryRun: false to apply. Optional: workspaceRoot (project root; default: ATOMIX_PROJECT_ROOT env or process.cwd()), output (default ./tokens.css), format (default css), skipTokens, dryRun.",
|
|
2443
1746
|
inputSchema: {
|
|
2444
1747
|
type: "object",
|
|
2445
1748
|
properties: {
|
|
@@ -2506,6 +1809,43 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2506
1809
|
}
|
|
2507
1810
|
}
|
|
2508
1811
|
];
|
|
1812
|
+
if (cachedMcpTier === "pro") {
|
|
1813
|
+
toolsList.push({
|
|
1814
|
+
name: "designInFigma",
|
|
1815
|
+
description: "Design UI in the connected Figma file using the design system tokens. Call with action:'catalog' to discover available bridge methods, their parameters, and the file's variables/styles. Call with action:'query' to read from Figma: get_selection (current selection), get_node_info (nodeId), get_design_screenshot (frameId; returns PNG image for review). Call with action:'execute' and an array of steps to create the design on canvas. Requires the Atomix Figma plugin to be connected.",
|
|
1816
|
+
inputSchema: {
|
|
1817
|
+
type: "object",
|
|
1818
|
+
properties: {
|
|
1819
|
+
action: {
|
|
1820
|
+
type: "string",
|
|
1821
|
+
enum: ["catalog", "query", "execute"],
|
|
1822
|
+
description: "catalog = discover methods + file context; query = read selection/node/screenshot; execute = run design steps"
|
|
1823
|
+
},
|
|
1824
|
+
queryMethod: {
|
|
1825
|
+
type: "string",
|
|
1826
|
+
description: "Required when action is 'query'. One of: get_selection, get_node_info, get_document_info, get_design_screenshot, get_figma_variables_and_styles, list_local_components, get_component_catalog, get_variable_collection_modes, get_frame_variable_mode."
|
|
1827
|
+
},
|
|
1828
|
+
queryParams: {
|
|
1829
|
+
type: "object",
|
|
1830
|
+
description: "Optional params for query. get_node_info needs { nodeId }. get_design_screenshot needs { frameId } and optional { scale }."
|
|
1831
|
+
},
|
|
1832
|
+
steps: {
|
|
1833
|
+
type: "array",
|
|
1834
|
+
description: "Required when action is 'execute'. Array of { method, params } design commands.",
|
|
1835
|
+
items: {
|
|
1836
|
+
type: "object",
|
|
1837
|
+
properties: {
|
|
1838
|
+
method: { type: "string" },
|
|
1839
|
+
params: { type: "object" }
|
|
1840
|
+
},
|
|
1841
|
+
required: ["method"]
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
},
|
|
1845
|
+
required: ["action"]
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
2509
1849
|
return { tools: toolsList };
|
|
2510
1850
|
});
|
|
2511
1851
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -2648,7 +1988,7 @@ Version: ${designSystemData.meta.version}`,
|
|
|
2648
1988
|
` Tokens: ${tokenCount} (${deprecatedCount} deprecated preserved)`,
|
|
2649
1989
|
changeLine,
|
|
2650
1990
|
"",
|
|
2651
|
-
"Would write skills: .cursor/skills/atomix-ds/SKILL.md",
|
|
1991
|
+
"Would write skills: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/figma-design-SKILL.md",
|
|
2652
1992
|
"",
|
|
2653
1993
|
"Call syncAll again with dryRun: false to apply."
|
|
2654
1994
|
].filter(Boolean).join("\n");
|
|
@@ -2681,7 +2021,8 @@ Version: ${designSystemData.meta.version}`,
|
|
|
2681
2021
|
hasRefactorRecommendation: !!lastSyncAffectedTokens?.removed.length,
|
|
2682
2022
|
deprecatedTokenCount: lastSyncAffectedTokens?.removed.length || 0
|
|
2683
2023
|
});
|
|
2684
|
-
|
|
2024
|
+
const tokenSyncChangeSummary = fileExists && ["css", "scss", "less"].includes(format) ? { fileExisted: true, previousVarCount: existingTokens.size, currentVarCount: tokenCount } : { fileExisted: false, previousVarCount: void 0, currentVarCount: tokenCount };
|
|
2025
|
+
return { responseText: response, rulesResults, validation, tokenSyncChangeSummary };
|
|
2685
2026
|
}
|
|
2686
2027
|
switch (name) {
|
|
2687
2028
|
case "getToken": {
|
|
@@ -3113,10 +2454,12 @@ Version: ${designSystemData.meta.version}`,
|
|
|
3113
2454
|
const parts = [dryRun ? "[DRY RUN] syncAll report (no files written)." : "\u2713 syncAll complete."];
|
|
3114
2455
|
let tokenResponseText = "";
|
|
3115
2456
|
const allValidation = [];
|
|
2457
|
+
let tokenSyncChangeSummary;
|
|
3116
2458
|
if (!skipTokens) {
|
|
3117
2459
|
const result = await performTokenSyncAndRules(data, output, format, dryRun, projectRoot);
|
|
3118
2460
|
tokenResponseText = result.responseText;
|
|
3119
2461
|
allValidation.push(...result.validation);
|
|
2462
|
+
tokenSyncChangeSummary = result.tokenSyncChangeSummary;
|
|
3120
2463
|
if (dryRun) {
|
|
3121
2464
|
parts.push(`Would write tokens: ${output} (${format})`);
|
|
3122
2465
|
} else {
|
|
@@ -3126,10 +2469,11 @@ Version: ${designSystemData.meta.version}`,
|
|
|
3126
2469
|
const dsVersion = String(data.meta.version ?? "1.0.0");
|
|
3127
2470
|
const dsExportedAt = data.meta.exportedAt;
|
|
3128
2471
|
const skillsDir = path.resolve(projectRoot, ".cursor/skills/atomix-ds");
|
|
3129
|
-
const skillPath1 = path.join(skillsDir, "SKILL.md");
|
|
3130
2472
|
const manifestPath = path.resolve(projectRoot, "atomix-dependencies.json");
|
|
2473
|
+
const dependencySkills = getSyncDependencySkills(data, dsVersion, dsExportedAt);
|
|
3131
2474
|
if (dryRun) {
|
|
3132
|
-
|
|
2475
|
+
const skillList = dependencySkills.map((s) => s.path).join(", ");
|
|
2476
|
+
parts.push(`Would write skills: ${skillList}`);
|
|
3133
2477
|
parts.push("Would write manifest: atomix-dependencies.json");
|
|
3134
2478
|
const reportText = [parts.join("\n"), tokenResponseText].filter(Boolean).join("\n\n---\n\n");
|
|
3135
2479
|
return {
|
|
@@ -3139,10 +2483,18 @@ ${reportText}` }]
|
|
|
3139
2483
|
};
|
|
3140
2484
|
}
|
|
3141
2485
|
if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
|
|
3142
|
-
const
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
2486
|
+
const skillsWritten = [];
|
|
2487
|
+
for (const sk of dependencySkills) {
|
|
2488
|
+
const absPath = path.resolve(projectRoot, sk.path);
|
|
2489
|
+
const existed = fs.existsSync(absPath);
|
|
2490
|
+
const prevVersion = existed ? readSkillVersionFromFile(absPath) : null;
|
|
2491
|
+
const fromLabel = existed ? prevVersion ? `DS v${prevVersion}` : "existing" : "missing";
|
|
2492
|
+
fs.writeFileSync(absPath, sk.content);
|
|
2493
|
+
allValidation.push({ path: absPath, status: fs.existsSync(absPath) ? "OK" : "FAIL", detail: "Written." });
|
|
2494
|
+
skillsWritten.push({ path: sk.path, shortName: sk.shortName, from: fromLabel });
|
|
2495
|
+
}
|
|
2496
|
+
const skillShortNames = skillsWritten.map((s) => s.shortName).join(" + ");
|
|
2497
|
+
parts.push(`Skills: ${skillShortNames} (DS v${dsVersion})`);
|
|
3146
2498
|
const tokens = data.tokens;
|
|
3147
2499
|
const typography = tokens?.typography;
|
|
3148
2500
|
const fontFamily = typography?.fontFamily;
|
|
@@ -3161,6 +2513,10 @@ ${reportText}` }]
|
|
|
3161
2513
|
};
|
|
3162
2514
|
const lib = icons?.library || "lucide";
|
|
3163
2515
|
const iconPkgs = ICON_PACKAGES[lib] || ICON_PACKAGES.lucide;
|
|
2516
|
+
const skillsManifestEntry = { skill: ".cursor/skills/atomix-ds/SKILL.md", syncedAtVersion: String(data.meta.version ?? "1.0.0") };
|
|
2517
|
+
if (dependencySkills.some((s) => s.shortName === "figma-design-SKILL.md")) {
|
|
2518
|
+
skillsManifestEntry.figmaDesignSkill = ".cursor/skills/atomix-ds/figma-design-SKILL.md";
|
|
2519
|
+
}
|
|
3164
2520
|
const manifest = {
|
|
3165
2521
|
designSystem: { name: data.meta.name, version: data.meta.version },
|
|
3166
2522
|
tokenFile: skipTokens ? void 0 : output,
|
|
@@ -3171,23 +2527,67 @@ ${reportText}` }]
|
|
|
3171
2527
|
strokeWidthValue: icons?.strokeWidth
|
|
3172
2528
|
},
|
|
3173
2529
|
fonts: { families: fontNames },
|
|
3174
|
-
skills:
|
|
3175
|
-
skill: ".cursor/skills/atomix-ds/SKILL.md",
|
|
3176
|
-
syncedAtVersion: data.meta.version ?? "1.0.0"
|
|
3177
|
-
}
|
|
2530
|
+
skills: skillsManifestEntry
|
|
3178
2531
|
};
|
|
2532
|
+
const manifestExisted = fs.existsSync(manifestPath);
|
|
3179
2533
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
3180
2534
|
allValidation.push({ path: manifestPath, status: fs.existsSync(manifestPath) ? "OK" : "FAIL", detail: "Written." });
|
|
3181
2535
|
parts.push("Manifest: atomix-dependencies.json (icons, fonts, skill paths)");
|
|
2536
|
+
const changeRows = [];
|
|
2537
|
+
if (!skipTokens && tokenSyncChangeSummary) {
|
|
2538
|
+
const fromStr = tokenSyncChangeSummary.fileExisted && tokenSyncChangeSummary.previousVarCount != null ? `${tokenSyncChangeSummary.previousVarCount} tokens in ${output}` : "missing";
|
|
2539
|
+
changeRows.push({
|
|
2540
|
+
what: "Tokens",
|
|
2541
|
+
from: fromStr,
|
|
2542
|
+
to: `${tokenSyncChangeSummary.currentVarCount} tokens in ${output} (${format})`
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
2545
|
+
const skillsFrom = skillsWritten.length ? skillsWritten.every((s) => s.from === "missing") ? "missing" : "updated" : "";
|
|
2546
|
+
if (skillsWritten.length > 0) {
|
|
2547
|
+
changeRows.push({
|
|
2548
|
+
what: "Skills",
|
|
2549
|
+
from: skillsFrom,
|
|
2550
|
+
to: `${skillShortNames} (DS v${dsVersion})`
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
changeRows.push({
|
|
2554
|
+
what: "Manifest",
|
|
2555
|
+
from: manifestExisted ? "existing" : "missing",
|
|
2556
|
+
to: "atomix-dependencies.json"
|
|
2557
|
+
});
|
|
2558
|
+
if (lastSyncAffectedTokens && (lastSyncAffectedTokens.modified.length > 0 || lastSyncAffectedTokens.added.length > 0 || lastSyncAffectedTokens.removed.length > 0)) {
|
|
2559
|
+
const mod = lastSyncAffectedTokens.modified;
|
|
2560
|
+
const add = lastSyncAffectedTokens.added;
|
|
2561
|
+
const rem = lastSyncAffectedTokens.removed;
|
|
2562
|
+
const tokenNames = [
|
|
2563
|
+
...mod.map((m) => m.token),
|
|
2564
|
+
...add,
|
|
2565
|
+
...rem.map((r) => r.token)
|
|
2566
|
+
];
|
|
2567
|
+
const fromParts = [];
|
|
2568
|
+
if (mod.length) fromParts.push(`${mod.length} modified`);
|
|
2569
|
+
if (add.length) fromParts.push(`${add.length} added`);
|
|
2570
|
+
if (rem.length) fromParts.push(`${rem.length} removed`);
|
|
2571
|
+
const toDetail = tokenNames.length <= 8 ? tokenNames.join(", ") : `${tokenNames.length} tokens (${tokenNames.slice(0, 4).join(", ")}...)`;
|
|
2572
|
+
changeRows.push({
|
|
2573
|
+
what: "Token changes (this run)",
|
|
2574
|
+
from: fromParts.join(", ") || "\u2014",
|
|
2575
|
+
to: toDetail
|
|
2576
|
+
});
|
|
2577
|
+
if (mod.length > 0 && mod.length <= 15) {
|
|
2578
|
+
mod.forEach((m) => {
|
|
2579
|
+
changeRows.push({ what: ` ${m.token}`, from: m.oldValue, to: m.newValue });
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
const changesList = "Report the following changes to the user (From \u2192 To). Do not mention what did not change.\n\n**Changes applied**\n\n" + changeRows.map((r) => `\u2022 **${r.what}:** ${r.from} \u2192 ${r.to}`).join("\n");
|
|
3182
2584
|
const summary = parts.join("\n");
|
|
3183
2585
|
const validationBlock = formatValidationBlock(allValidation);
|
|
3184
2586
|
const hasFailure = allValidation.some((e) => e.status === "FAIL");
|
|
3185
2587
|
const resultLine = hasFailure ? "syncAllResult: FAIL \u2014 Check VALIDATION section below. Do not report success to the user.\n\n" : "syncAllResult: OK\n\n";
|
|
3186
|
-
const fullText = resultLine + (tokenResponseText ?
|
|
3187
|
-
|
|
3188
|
-
---
|
|
2588
|
+
const fullText = resultLine + changesList + "\n\n" + (tokenResponseText ? `---
|
|
3189
2589
|
|
|
3190
|
-
${tokenResponseText}
|
|
2590
|
+
${tokenResponseText}` : "") + validationBlock;
|
|
3191
2591
|
return {
|
|
3192
2592
|
content: [{ type: "text", text: fullText }]
|
|
3193
2593
|
};
|
|
@@ -3225,10 +2625,17 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
|
|
|
3225
2625
|
families: fontNames,
|
|
3226
2626
|
performanceHint: "Link fonts via URL (e.g. Google Fonts <link> or CSS @import); no need to download font files or add them to the repo. Prefer font-display: swap when possible. You must also build a complete typeset CSS: call listTypesets to get every typeset from the design system, then emit one CSS class per typeset (do not skip any). For each class set font-family, font-size, font-weight, line-height, letter-spacing; when the typeset has text-transform or text-decoration, set those too so the result is 1:1 with the DS. Use the CSS variable names returned by listTypesets. Do not create a file that only contains a font import."
|
|
3227
2627
|
},
|
|
3228
|
-
skill: {
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
2628
|
+
skill: (() => {
|
|
2629
|
+
const list = getSyncDependencySkills(data, String(data.meta.version ?? "1.0.0"), data.meta.exportedAt);
|
|
2630
|
+
const generic = list.find((s) => s.shortName === "SKILL.md");
|
|
2631
|
+
return generic ? { path: generic.path, content: GENERIC_SKILL_MD } : { path: ".cursor/skills/atomix-ds/SKILL.md", content: GENERIC_SKILL_MD };
|
|
2632
|
+
})(),
|
|
2633
|
+
...getEffectiveTier() === "pro" ? {
|
|
2634
|
+
skillFigmaDesign: {
|
|
2635
|
+
path: ".cursor/skills/atomix-ds/figma-design-SKILL.md",
|
|
2636
|
+
content: FIGMA_DESIGN_SKILL_MD
|
|
2637
|
+
}
|
|
2638
|
+
} : {},
|
|
3232
2639
|
tokenFiles: {
|
|
3233
2640
|
files: ["tokens.css", "tokens.json"],
|
|
3234
2641
|
copyInstructions: "Call the syncAll MCP tool to create the token file, skills, and atomix-dependencies.json; do not only suggest the user run sync later."
|
|
@@ -3453,13 +2860,282 @@ ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
|
|
|
3453
2860
|
...out.error ? { isError: true } : {}
|
|
3454
2861
|
};
|
|
3455
2862
|
}
|
|
2863
|
+
case "designInFigma": {
|
|
2864
|
+
if (cachedMcpTier !== "pro") {
|
|
2865
|
+
return {
|
|
2866
|
+
content: [{ type: "text", text: JSON.stringify({ error: "designInFigma requires a Pro subscription. Upgrade at https://atomix.studio" }, null, 2) }],
|
|
2867
|
+
isError: true
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
const action = args?.action;
|
|
2871
|
+
if (action === "catalog") {
|
|
2872
|
+
try {
|
|
2873
|
+
const reachable = await isBridgeReachable();
|
|
2874
|
+
let fileCtx;
|
|
2875
|
+
if (reachable) {
|
|
2876
|
+
try {
|
|
2877
|
+
const raw = await sendBridgeRequest("get_figma_variables_and_styles", {});
|
|
2878
|
+
fileCtx = raw;
|
|
2879
|
+
} catch {
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
const localVars = [];
|
|
2883
|
+
const libVars = [];
|
|
2884
|
+
const textStyleNames = [];
|
|
2885
|
+
const effectStyleNames = [];
|
|
2886
|
+
if (fileCtx) {
|
|
2887
|
+
for (const coll of fileCtx.variableCollections ?? []) {
|
|
2888
|
+
for (const v of coll.variables) localVars.push(v.name);
|
|
2889
|
+
}
|
|
2890
|
+
for (const coll of fileCtx.variableCollectionsLibrary ?? []) {
|
|
2891
|
+
for (const v of coll.variables) libVars.push(v.name);
|
|
2892
|
+
}
|
|
2893
|
+
for (const s of fileCtx.textStyles ?? []) textStyleNames.push(s.name);
|
|
2894
|
+
for (const s of fileCtx.effectStyles ?? []) effectStyleNames.push(s.name);
|
|
2895
|
+
}
|
|
2896
|
+
const catalogPayload = formatCatalogForMCP(FIGMA_DESIGN_CATALOG, {
|
|
2897
|
+
localVariables: localVars,
|
|
2898
|
+
libraryVariables: libVars,
|
|
2899
|
+
textStyles: textStyleNames,
|
|
2900
|
+
effectStyles: effectStyleNames
|
|
2901
|
+
});
|
|
2902
|
+
let designAssets = void 0;
|
|
2903
|
+
if (reachable) {
|
|
2904
|
+
try {
|
|
2905
|
+
const raw = await sendBridgeRequest("get_component_catalog", {});
|
|
2906
|
+
if (raw && typeof raw === "object") {
|
|
2907
|
+
designAssets = raw;
|
|
2908
|
+
}
|
|
2909
|
+
} catch {
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
return {
|
|
2913
|
+
content: [{
|
|
2914
|
+
type: "text",
|
|
2915
|
+
text: JSON.stringify({
|
|
2916
|
+
...catalogPayload,
|
|
2917
|
+
pluginConnected: reachable,
|
|
2918
|
+
...designAssets ? { designAssets } : {},
|
|
2919
|
+
...reachable ? {
|
|
2920
|
+
queryMethods: getQueryMethodNames(),
|
|
2921
|
+
queryHint: "Use action:'query' with queryMethod (e.g. get_selection, get_node_info, get_design_screenshot) and optional queryParams to read selection, node details, or a frame screenshot for review."
|
|
2922
|
+
} : {},
|
|
2923
|
+
...reachable ? {} : { hint: "Figma plugin not connected. Open Figma, run Atomix plugin, and tap Connect." }
|
|
2924
|
+
}, null, 2)
|
|
2925
|
+
}]
|
|
2926
|
+
};
|
|
2927
|
+
} catch (e) {
|
|
2928
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2929
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: msg }, null, 2) }], isError: true };
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
if (action === "query") {
|
|
2933
|
+
const queryMethodRaw = args?.queryMethod;
|
|
2934
|
+
const queryParams = args?.queryParams ?? {};
|
|
2935
|
+
if (!queryMethodRaw || typeof queryMethodRaw !== "string") {
|
|
2936
|
+
return {
|
|
2937
|
+
content: [{ type: "text", text: JSON.stringify({ error: "query requires queryMethod (e.g. get_selection, get_node_info, get_design_screenshot)" }, null, 2) }],
|
|
2938
|
+
isError: true
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
const queryMethod = queryMethodRaw.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
2942
|
+
const allowedQueryMethods = new Set(getQueryMethodNames());
|
|
2943
|
+
if (!allowedQueryMethods.has(queryMethod)) {
|
|
2944
|
+
return {
|
|
2945
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Unknown query method "${queryMethod}". Allowed: ${[...allowedQueryMethods].join(", ")}` }, null, 2) }],
|
|
2946
|
+
isError: true
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
2949
|
+
try {
|
|
2950
|
+
const reachable = await isBridgeReachable();
|
|
2951
|
+
if (!reachable) {
|
|
2952
|
+
return {
|
|
2953
|
+
content: [{
|
|
2954
|
+
type: "text",
|
|
2955
|
+
text: JSON.stringify({
|
|
2956
|
+
error: "Figma plugin not connected.",
|
|
2957
|
+
bridgeNotRunning: true,
|
|
2958
|
+
agentInstruction: "Run the Atomix plugin in Figma and tap Connect, then call designInFigma with action:'query' again."
|
|
2959
|
+
}, null, 2)
|
|
2960
|
+
}],
|
|
2961
|
+
isError: true
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2964
|
+
const result = await sendBridgeRequest(queryMethod, queryParams);
|
|
2965
|
+
const content = [];
|
|
2966
|
+
const summary = { method: queryMethod, success: true };
|
|
2967
|
+
if (result && typeof result === "object" && "imageBase64" in result && typeof result.imageBase64 === "string") {
|
|
2968
|
+
summary.screenshot = "included below (base64 PNG)";
|
|
2969
|
+
summary.format = result.format ?? "PNG";
|
|
2970
|
+
summary.scale = result.scale;
|
|
2971
|
+
content.push({ type: "text", text: JSON.stringify(summary, null, 2) });
|
|
2972
|
+
content.push({
|
|
2973
|
+
type: "image",
|
|
2974
|
+
data: result.imageBase64,
|
|
2975
|
+
mimeType: "image/png"
|
|
2976
|
+
});
|
|
2977
|
+
} else {
|
|
2978
|
+
summary.result = result;
|
|
2979
|
+
content.push({ type: "text", text: JSON.stringify(summary, null, 2) });
|
|
2980
|
+
}
|
|
2981
|
+
return { content };
|
|
2982
|
+
} catch (e) {
|
|
2983
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2984
|
+
return { content: [{ type: "text", text: JSON.stringify({ method: queryMethod, success: false, error: msg }, null, 2) }], isError: true };
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
if (action === "execute") {
|
|
2988
|
+
const rawSteps = args?.steps;
|
|
2989
|
+
if (!rawSteps || !Array.isArray(rawSteps) || rawSteps.length === 0) {
|
|
2990
|
+
return {
|
|
2991
|
+
content: [{ type: "text", text: JSON.stringify({ error: "steps array is required for action:'execute'" }, null, 2) }],
|
|
2992
|
+
isError: true
|
|
2993
|
+
};
|
|
2994
|
+
}
|
|
2995
|
+
try {
|
|
2996
|
+
const reachable = await isBridgeReachable();
|
|
2997
|
+
if (!reachable) {
|
|
2998
|
+
return {
|
|
2999
|
+
content: [{
|
|
3000
|
+
type: "text",
|
|
3001
|
+
text: JSON.stringify({
|
|
3002
|
+
error: "Figma plugin not connected.",
|
|
3003
|
+
bridgeNotRunning: true,
|
|
3004
|
+
agentInstruction: "Run the Atomix plugin in Figma and tap Connect, then call designInFigma again."
|
|
3005
|
+
}, null, 2)
|
|
3006
|
+
}],
|
|
3007
|
+
isError: true
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
const designMethods = /* @__PURE__ */ new Set([
|
|
3011
|
+
...getDesignMethodNames(),
|
|
3012
|
+
"list_local_components",
|
|
3013
|
+
"get_component_catalog",
|
|
3014
|
+
"get_design_screenshot",
|
|
3015
|
+
"get_variable_collection_modes",
|
|
3016
|
+
"get_frame_variable_mode"
|
|
3017
|
+
]);
|
|
3018
|
+
const warnings = [];
|
|
3019
|
+
let fileCtx;
|
|
3020
|
+
try {
|
|
3021
|
+
const raw = await sendBridgeRequest("get_figma_variables_and_styles", {});
|
|
3022
|
+
fileCtx = raw;
|
|
3023
|
+
} catch {
|
|
3024
|
+
}
|
|
3025
|
+
const resolvers = fileCtx ? buildResolvers(fileCtx) : null;
|
|
3026
|
+
const isFigmaNodeId = (s) => /^\d+:\d+$/.test(s);
|
|
3027
|
+
let rootFrameId = null;
|
|
3028
|
+
let lastCreatedFrameNodeId = null;
|
|
3029
|
+
const namedNodeIds = /* @__PURE__ */ new Map();
|
|
3030
|
+
const results = [];
|
|
3031
|
+
for (let i = 0; i < rawSteps.length; i++) {
|
|
3032
|
+
const step = rawSteps[i];
|
|
3033
|
+
const method = (step.method ?? "").replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
3034
|
+
if (!designMethods.has(method)) {
|
|
3035
|
+
warnings.push({ step: i, method, issue: `Unknown method "${method}" \u2014 skipped` });
|
|
3036
|
+
continue;
|
|
3037
|
+
}
|
|
3038
|
+
let params = { ...step.params ?? {} };
|
|
3039
|
+
if (resolvers) {
|
|
3040
|
+
params = resolveStepParams(params, resolvers);
|
|
3041
|
+
}
|
|
3042
|
+
if (rootFrameId) {
|
|
3043
|
+
const parentId = params.parentId;
|
|
3044
|
+
const frameId = params.frameId;
|
|
3045
|
+
if (typeof parentId === "string" && !isFigmaNodeId(parentId)) {
|
|
3046
|
+
params.parentId = namedNodeIds.get(parentId) ?? rootFrameId;
|
|
3047
|
+
}
|
|
3048
|
+
if (typeof frameId === "string" && !isFigmaNodeId(frameId)) {
|
|
3049
|
+
params.frameId = namedNodeIds.get(frameId) ?? rootFrameId;
|
|
3050
|
+
}
|
|
3051
|
+
if (method === "finalize_design_frame" && !params.frameId) {
|
|
3052
|
+
params.frameId = rootFrameId;
|
|
3053
|
+
}
|
|
3054
|
+
const needsParent = method.startsWith("design_create_") || method === "design_create_component";
|
|
3055
|
+
if (needsParent && !params.parentId) {
|
|
3056
|
+
params.parentId = rootFrameId;
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
const nodeId = params.nodeId;
|
|
3060
|
+
if (typeof nodeId === "string" && nodeId && !isFigmaNodeId(nodeId)) {
|
|
3061
|
+
const resolved = namedNodeIds.get(nodeId) ?? lastCreatedFrameNodeId ?? rootFrameId;
|
|
3062
|
+
params.nodeId = resolved ?? nodeId;
|
|
3063
|
+
}
|
|
3064
|
+
const needsNode = [
|
|
3065
|
+
"design_set_auto_layout",
|
|
3066
|
+
"design_set_layout_sizing",
|
|
3067
|
+
"design_set_effects",
|
|
3068
|
+
"design_set_strokes",
|
|
3069
|
+
"design_resize_node",
|
|
3070
|
+
"design_set_resize_constraints",
|
|
3071
|
+
"design_set_layout_constraints",
|
|
3072
|
+
"design_set_node_position",
|
|
3073
|
+
"design_set_text_properties"
|
|
3074
|
+
].includes(method);
|
|
3075
|
+
if (needsNode && !params.nodeId) {
|
|
3076
|
+
params.nodeId = lastCreatedFrameNodeId ?? rootFrameId;
|
|
3077
|
+
}
|
|
3078
|
+
const childId = params.childId;
|
|
3079
|
+
if (typeof childId === "string" && childId && !isFigmaNodeId(childId)) {
|
|
3080
|
+
params.childId = namedNodeIds.get(childId) ?? childId;
|
|
3081
|
+
}
|
|
3082
|
+
try {
|
|
3083
|
+
const response = await sendBridgeRequest(method, params);
|
|
3084
|
+
const res = response;
|
|
3085
|
+
if (method === "create_design_placeholder" && typeof res?.frameId === "string") {
|
|
3086
|
+
rootFrameId = res.frameId;
|
|
3087
|
+
}
|
|
3088
|
+
const isCreateMethod = method.startsWith("design_create_") || method === "design_convert_to_component" || method === "design_combine_as_variants" || method === "design_create_frame_from_preset" || method === "design_group_nodes";
|
|
3089
|
+
if (isCreateMethod && typeof res?.nodeId === "string") {
|
|
3090
|
+
const createdNodeId = res.nodeId;
|
|
3091
|
+
if (method === "design_create_frame" || method === "design_create_frame_from_preset") {
|
|
3092
|
+
lastCreatedFrameNodeId = createdNodeId;
|
|
3093
|
+
if (!rootFrameId && res?.isRoot === true) {
|
|
3094
|
+
rootFrameId = createdNodeId;
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
const stepName = step.params?.name ?? "";
|
|
3098
|
+
if (stepName) namedNodeIds.set(stepName, createdNodeId);
|
|
3099
|
+
}
|
|
3100
|
+
results.push({ step: i, method, result: response });
|
|
3101
|
+
} catch (e) {
|
|
3102
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
3103
|
+
results.push({ step: i, method, error: errMsg });
|
|
3104
|
+
if (!rootFrameId && (method === "design_create_frame" || method === "create_design_placeholder")) break;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
const errorCount = results.filter((r) => r.error).length;
|
|
3108
|
+
const success = errorCount === 0;
|
|
3109
|
+
return {
|
|
3110
|
+
content: [{
|
|
3111
|
+
type: "text",
|
|
3112
|
+
text: JSON.stringify({
|
|
3113
|
+
success,
|
|
3114
|
+
stepsExecuted: results.length,
|
|
3115
|
+
errors: errorCount,
|
|
3116
|
+
warnings,
|
|
3117
|
+
results
|
|
3118
|
+
}, null, 2)
|
|
3119
|
+
}],
|
|
3120
|
+
...errorCount > 0 ? { isError: true } : {}
|
|
3121
|
+
};
|
|
3122
|
+
} catch (e) {
|
|
3123
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3124
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: msg }, null, 2) }], isError: true };
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
return {
|
|
3128
|
+
content: [{ type: "text", text: JSON.stringify({ error: "action must be 'catalog', 'query', or 'execute'" }, null, 2) }],
|
|
3129
|
+
isError: true
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3456
3132
|
default:
|
|
3457
3133
|
return {
|
|
3458
3134
|
content: [{
|
|
3459
3135
|
type: "text",
|
|
3460
3136
|
text: JSON.stringify({
|
|
3461
3137
|
error: `Unknown tool: ${name}`,
|
|
3462
|
-
availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "getMcpVersion", "syncToFigma"]
|
|
3138
|
+
availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "getMcpVersion", "syncToFigma", "designInFigma"]
|
|
3463
3139
|
}, null, 2)
|
|
3464
3140
|
}]
|
|
3465
3141
|
};
|
|
@@ -3501,6 +3177,43 @@ ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
|
|
|
3501
3177
|
};
|
|
3502
3178
|
}
|
|
3503
3179
|
});
|
|
3180
|
+
function getEffectiveTier() {
|
|
3181
|
+
return cachedMcpTier;
|
|
3182
|
+
}
|
|
3183
|
+
function getSyncDependencySkills(data, dsVersion, dsExportedAt) {
|
|
3184
|
+
const tier = getEffectiveTier();
|
|
3185
|
+
const skills = [];
|
|
3186
|
+
const genericWithVersion = injectSkillVersion(GENERIC_SKILL_MD, dsVersion, dsExportedAt);
|
|
3187
|
+
skills.push({
|
|
3188
|
+
path: ".cursor/skills/atomix-ds/SKILL.md",
|
|
3189
|
+
content: genericWithVersion,
|
|
3190
|
+
shortName: "SKILL.md"
|
|
3191
|
+
});
|
|
3192
|
+
if (tier === "pro") {
|
|
3193
|
+
const figmaSkillWithFrontmatter = `---
|
|
3194
|
+
name: atomix-figma-design
|
|
3195
|
+
description: Figma design skill \u2014 create high-quality, token-bound designs in Figma from any reference (screenshots, images, code snippets, URLs). Covers mobile, web, token application, and component/variant creation. Use with syncToFigma and designInFigma MCP tools when the Atomix plugin is connected.
|
|
3196
|
+
---
|
|
3197
|
+
${FIGMA_DESIGN_SKILL_MD}`;
|
|
3198
|
+
const figmaSkillWithVersion = injectSkillVersion(figmaSkillWithFrontmatter, dsVersion, dsExportedAt);
|
|
3199
|
+
skills.push({
|
|
3200
|
+
path: ".cursor/skills/atomix-ds/figma-design-SKILL.md",
|
|
3201
|
+
content: figmaSkillWithVersion,
|
|
3202
|
+
shortName: "figma-design-SKILL.md"
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
3205
|
+
return skills;
|
|
3206
|
+
}
|
|
3207
|
+
function readSkillVersionFromFile(filePath) {
|
|
3208
|
+
if (!fs.existsSync(filePath)) return null;
|
|
3209
|
+
try {
|
|
3210
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
3211
|
+
const match = raw.match(/atomixDsVersion:\s*["']([^"']+)["']/);
|
|
3212
|
+
return match ? match[1] : null;
|
|
3213
|
+
} catch {
|
|
3214
|
+
return null;
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3504
3217
|
function injectSkillVersion(content, version, exportedAt) {
|
|
3505
3218
|
const endOfFrontmatter = content.indexOf("\n---\n", 3);
|
|
3506
3219
|
if (endOfFrontmatter === -1) return content;
|
|
@@ -3593,7 +3306,7 @@ When a semantic token doesn't exist for your use case, use the closest primitive
|
|
|
3593
3306
|
|
|
3594
3307
|
### 4. Syncing tokens to a file
|
|
3595
3308
|
|
|
3596
|
-
\`syncAll({ output?, format?, skipTokens? })\` \u2014 writes tokens to a file (default \`./tokens.css\`), skills (.cursor/skills/atomix-ds/SKILL.md), and manifest. Use \`skipTokens: true\` to only write skills and manifest.
|
|
3309
|
+
\`syncAll({ output?, format?, skipTokens? })\` \u2014 writes tokens to a file (default \`./tokens.css\`), skills (.cursor/skills/atomix-ds/SKILL.md and figma-design-SKILL.md), and manifest. Use \`skipTokens: true\` to only write skills and manifest.
|
|
3597
3310
|
|
|
3598
3311
|
## Workflow
|
|
3599
3312
|
|
|
@@ -3620,7 +3333,7 @@ If no token matches, call \`searchTokens\` to find the closest option. Never inv
|
|
|
3620
3333
|
- **Fetch first:** Always call getRules and/or listTokens before writing any styles, regardless of platform or framework.
|
|
3621
3334
|
- **Semantic over primitive:** Prefer tokens that describe purpose (\`text-primary\`, \`bg-surface\`) over tokens that describe appearance (\`neutral.900\`, \`white\`).
|
|
3622
3335
|
- **Icons:** Size via \`getToken("sizing.icon.sm")\`; stroke width via \`getToken("icons.strokeWidth")\` when the DS defines it.
|
|
3623
|
-
- **Typography:** Use
|
|
3336
|
+
- **Typography:** Use **typeset classes** (e.g. from \`typesets.css\` or \`listTypesets()\` output \u2014 \`typeset-heading-h1\`, \`typeset-body-md-regular\`, etc.) for all text; do not re-declare font-family, font-size, font-weight, line-height, or letter-spacing in component CSS, which duplicates the design system. For global typeset output, call **listTypesets** and emit every entry; include text-transform and text-decoration for 1:1 match.
|
|
3624
3337
|
- **No guessing:** If a value is not in the rules or token list, call searchTokens or listTokens to find the closest match.
|
|
3625
3338
|
- **Platform agnostic:** Token values work across CSS, Tailwind, React Native, SwiftUI, Compose, Flutter, and any style system. Use the output format appropriate to your platform.
|
|
3626
3339
|
- **Version check:** If this file has frontmatter \`atomixDsVersion\`, compare to the version from **getDependencies** (\`meta.designSystemVersion\`). If the DS is newer, suggest running **syncAll** to update.
|
|
@@ -3912,6 +3625,9 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
|
3912
3625
|
{ name: "--refactor", description: "Migrate deprecated tokens in codebase. Run after /--sync." },
|
|
3913
3626
|
{ name: "--sync-to-figma", description: "Push this design system to Figma (variables, color + typography styles). Uses local bridge + plugin; no Figma token." }
|
|
3914
3627
|
];
|
|
3628
|
+
if (cachedMcpTier === "pro") {
|
|
3629
|
+
prompts.push({ name: "--design-in-figma", description: "Load Figma design skill and use designInFigma tool to design UI in the connected Figma file. Pro only." });
|
|
3630
|
+
}
|
|
3915
3631
|
return { prompts };
|
|
3916
3632
|
});
|
|
3917
3633
|
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
@@ -3928,7 +3644,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
3928
3644
|
}]
|
|
3929
3645
|
};
|
|
3930
3646
|
}
|
|
3931
|
-
const canonicalName = name === "--hello" ? "hello" : name === "--get-started" ? "atomix-setup" : name === "--rules" ? "design-system-rules" : name === "--sync" ? "sync" : name === "--refactor" ? "refactor" : name === "--sync-to-figma" || name === "syncToFigma" ? "sync-to-figma" : name;
|
|
3647
|
+
const canonicalName = name === "--hello" ? "hello" : name === "--get-started" ? "atomix-setup" : name === "--rules" ? "design-system-rules" : name === "--sync" ? "sync" : name === "--refactor" ? "refactor" : name === "--sync-to-figma" || name === "syncToFigma" ? "sync-to-figma" : name === "--design-in-figma" || name === "designInFigma" ? "design-in-figma" : name;
|
|
3932
3648
|
const shouldForceRefresh = [
|
|
3933
3649
|
"hello",
|
|
3934
3650
|
"atomix-setup",
|
|
@@ -3937,7 +3653,8 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
3937
3653
|
// --rules
|
|
3938
3654
|
"sync",
|
|
3939
3655
|
"refactor",
|
|
3940
|
-
"sync-to-figma"
|
|
3656
|
+
"sync-to-figma",
|
|
3657
|
+
"design-in-figma"
|
|
3941
3658
|
].includes(canonicalName);
|
|
3942
3659
|
let data = null;
|
|
3943
3660
|
let stats = null;
|
|
@@ -4297,6 +4014,80 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
|
|
|
4297
4014
|
]
|
|
4298
4015
|
});
|
|
4299
4016
|
}
|
|
4017
|
+
case "design-in-figma": {
|
|
4018
|
+
if (getEffectiveTier() !== "pro") {
|
|
4019
|
+
return withMcpNotice({
|
|
4020
|
+
description: "Design in Figma (Pro required)",
|
|
4021
|
+
messages: [
|
|
4022
|
+
{
|
|
4023
|
+
role: "user",
|
|
4024
|
+
content: {
|
|
4025
|
+
type: "text",
|
|
4026
|
+
text: "designInFigma requires a Pro subscription. Upgrade at https://atomix.studio to use Design in Figma."
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
]
|
|
4030
|
+
});
|
|
4031
|
+
}
|
|
4032
|
+
if (!data) {
|
|
4033
|
+
return withMcpNotice({
|
|
4034
|
+
description: "Design in Figma",
|
|
4035
|
+
messages: [
|
|
4036
|
+
{
|
|
4037
|
+
role: "user",
|
|
4038
|
+
content: {
|
|
4039
|
+
type: "text",
|
|
4040
|
+
text: "Failed to fetch design system. Check your --ds-id and --atomix-token configuration, then try /--design-in-figma again."
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
]
|
|
4044
|
+
});
|
|
4045
|
+
}
|
|
4046
|
+
const figmaSkillPath = path.resolve(process.cwd(), ".cursor/skills/atomix-ds/figma-design-SKILL.md");
|
|
4047
|
+
const skillExists = fs.existsSync(figmaSkillPath);
|
|
4048
|
+
const currentVersion = String(data.meta.version ?? "1.0.0");
|
|
4049
|
+
const fileVersion = skillExists ? readSkillVersionFromFile(figmaSkillPath) : null;
|
|
4050
|
+
const isOutdated = !skillExists || fileVersion === null || fileVersion !== currentVersion;
|
|
4051
|
+
if (isOutdated) {
|
|
4052
|
+
return withMcpNotice({
|
|
4053
|
+
description: "Figma design skill missing or outdated",
|
|
4054
|
+
messages: [
|
|
4055
|
+
{
|
|
4056
|
+
role: "user",
|
|
4057
|
+
content: {
|
|
4058
|
+
type: "text",
|
|
4059
|
+
text: skillExists ? `The Figma design skill at \`.cursor/skills/atomix-ds/figma-design-SKILL.md\` is outdated (design system version ${currentVersion}). Run **/--sync** to update the skill and other design system files, then run **/--design-in-figma** again.` : `The Figma design skill is missing. Run **/--sync** to write \`.cursor/skills/atomix-ds/figma-design-SKILL.md\` (and other design system files), then run **/--design-in-figma** again.`
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
]
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
4065
|
+
return withMcpNotice({
|
|
4066
|
+
description: "Load Figma design skill and use designInFigma tool",
|
|
4067
|
+
messages: [
|
|
4068
|
+
{
|
|
4069
|
+
role: "user",
|
|
4070
|
+
content: {
|
|
4071
|
+
type: "text",
|
|
4072
|
+
text: `You are using **/--design-in-figma**: load the Figma design skill below and use the **designInFigma** MCP tool to design UI in the connected Figma file.
|
|
4073
|
+
|
|
4074
|
+
**Instructions:**
|
|
4075
|
+
1. Treat the following as the Figma design skill (advisory and generative UI on the Figma canvas). Follow it when creating or editing designs.
|
|
4076
|
+
2. Call **designInFigma** with \`action: "catalog"\` first to discover available bridge methods, file variables/styles, and query/execute capabilities.
|
|
4077
|
+
3. Use \`action: "query"\` to read from Figma (e.g. get_selection, get_node_info, get_design_screenshot).
|
|
4078
|
+
4. Use \`action: "execute"\` with an array of steps to create or modify the design on the canvas.
|
|
4079
|
+
5. If the response includes \`bridgeNotRunning\` or \`pluginConnected: false\`, tell the user to run the Atomix plugin in Figma and tap Connect, then try again.
|
|
4080
|
+
|
|
4081
|
+
---
|
|
4082
|
+
|
|
4083
|
+
## Figma design skill
|
|
4084
|
+
|
|
4085
|
+
${FIGMA_DESIGN_SKILL_MD}`
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
]
|
|
4089
|
+
});
|
|
4090
|
+
}
|
|
4300
4091
|
case "refactor": {
|
|
4301
4092
|
const refactorOutput = args?.output || "./tokens.css";
|
|
4302
4093
|
const refactorFormat = args?.format || "css";
|
|
@@ -4443,8 +4234,8 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
|
|
|
4443
4234
|
|
|
4444
4235
|
- Resolve platform/stack: infer from the project (e.g. package.json, build.gradle, Xcode) or ask once: "Which platform? (e.g. web, Android, iOS)" and if relevant "Which stack? (e.g. React, Vue, Next, Swift, Kotlin)." Do not assume a default.
|
|
4445
4236
|
- Call **getDependencies** with \`platform\` and optional \`stack\`. If it fails, tell the user the design system could not be reached and stop.
|
|
4446
|
-
- Scan the repo for: .cursor/skills/atomix-ds/SKILL.md, a tokens file (e.g. tokens.css or src/tokens.css), icon package from getDependencies, font links. **Web:** note any existing CSS (globals.css, main.css, Tailwind, etc.). **Native:** note any theme/style files (SwiftUI, Android themes, Compose).
|
|
4447
|
-
- Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links,
|
|
4237
|
+
- Scan the repo for: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/figma-design-SKILL.md, a tokens file (e.g. tokens.css or src/tokens.css), icon package from getDependencies, font links. **Web:** note any existing CSS (globals.css, main.css, Tailwind, etc.). **Native:** note any theme/style files (SwiftUI, Android themes, Compose).
|
|
4238
|
+
- Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links, skills (SKILL.md and figma-design-SKILL.md), token files; for web, also include the **showcase page** (atomix-setup-showcase.html) if getDependencies returned a \`showcase\` object.
|
|
4448
4239
|
- Do not write, create, or add anything in Phase 1.
|
|
4449
4240
|
|
|
4450
4241
|
## Phase 2 \u2013 Report and ask
|
|
@@ -4457,8 +4248,8 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
|
|
|
4457
4248
|
|
|
4458
4249
|
- Run only when the user has said yes (all or specific items).
|
|
4459
4250
|
- For each approved item:
|
|
4460
|
-
- **
|
|
4461
|
-
- **Token file and skills in repo:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css") and **workspaceRoot** set to the absolute path of the current project/workspace root. This ensures .cursor/skills/atomix-ds/SKILL.md and atomix-dependencies.json are written inside the repo so they can be committed. You must call syncAll; do not only suggest the user run it later.
|
|
4251
|
+
- **Skills:** Prefer calling **syncAll** (it writes both skills into the repo). If writing manually, write getDependencies \`skill.content\` to \`skill.path\` and \`skillFigmaDesign.content\` to \`skillFigmaDesign.path\` under the project root.
|
|
4252
|
+
- **Token file and skills in repo:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css") and **workspaceRoot** set to the absolute path of the current project/workspace root. This ensures .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/figma-design-SKILL.md, and atomix-dependencies.json are written inside the repo so they can be committed. You must call syncAll; do not only suggest the user run it later.
|
|
4462
4253
|
- **Icon package:** Install per getDependencies. When rendering icons, apply the design system's icon tokens: use getToken(\`sizing.icon.*\`) or listTokens(\`sizing\`) for size, and getToken(\`icons.strokeWidth\`) for stroke width when the DS defines it; do not use hardcoded sizes or stroke widths.
|
|
4463
4254
|
- **Fonts and typeset:** Add font links (e.g. \`<link>\` or \`@import\` from Google Fonts). Then build a **complete typeset CSS**: call **listTypesets** to get every typeset from the owner's design system (do not skip any). Emit **one CSS rule per typeset** using the \`cssClass\` and the \`fontFamilyVar\`, \`fontSizeVar\`, \`fontWeightVar\`, \`lineHeightVar\` (and \`letterSpacingVar\`, \`textTransformVar\`, \`textDecorationVar\` when present) returned by listTypesets. Include text-transform and text-decoration when the typeset has them so the result is **1:1** with the design system. The typeset file must define the full type scale\u2014not only a font import. Do not create a CSS file that contains only a font import.
|
|
4464
4255
|
- **Showcase page (web only):** If platform is web and getDependencies returned a \`showcase\` object, create the file at \`showcase.path\` using \`showcase.template\`. Replace every placeholder per \`showcase.substitutionInstructions\`: TOKENS_CSS_PATH, TYPESETS_LINK, DS_NAME, HEADING_FONT_VAR, FONT_FAMILY_VAR, LARGEST_DISPLAY_TYPESET_CLASS, LARGEST_BODY_TYPESET_CLASS, BODY_TYPESET_CLASS, FONT_LINK_TAG, BRAND_PRIMARY_VAR, BUTTON_PADDING_VAR, BUTTON_HEIGHT_VAR, BUTTON_RADIUS_VAR, CIRCLE_PADDING_VAR, ICON_SIZE_VAR, CHECK_ICON_SVG (inline SVG from the design system icon library). The page uses semantic colors (mode-aware) and a Dark/Light toggle. Use only CSS variable names that exist in the synced token file. Do not change the HTML structure. After creating the file, launch it in the default browser (e.g. \`open atomix-setup-showcase.html\` on macOS, \`xdg-open atomix-setup-showcase.html\` on Linux, or the equivalent on Windows).
|
|
@@ -4544,6 +4335,7 @@ ${tokenSummary}
|
|
|
4544
4335
|
| **/--rules** | Governance rules for your AI tool (e.g. Cursor, Copilot, Windsurf). |
|
|
4545
4336
|
| **/--sync** | Sync tokens, rules, skills, and dependencies manifest (icons, fonts). Safe: adds new, updates existing, marks deprecated. |
|
|
4546
4337
|
| **/--sync-to-figma** | Push design system to Figma (variables, paint/text/effect styles). Uses built-in bridge + Atomix plugin; connect plugin in Figma then run. Available on all tiers. |
|
|
4338
|
+
| **/--design-in-figma** | Design in Figma: loads Figma design skill and use designInFigma tool (catalog, query, execute). Pro only. |
|
|
4547
4339
|
| **/--refactor** | Migrate deprecated tokens in codebase. Run after /--sync. |
|
|
4548
4340
|
|
|
4549
4341
|
**Suggested next step:** Run **/--get-started** to set up global styles, icons, fonts, and token files; the AI will list options and ask before adding anything.
|