@atomixstudio/mcp 1.0.34 → 1.0.37
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 +5 -25
- package/dist/index.js +119 -1152
- package/dist/index.js.map +1 -1
- package/package.json +3 -5
- package/dist/chunk-CE6J5MJX.js +0 -49
- package/dist/chunk-CE6J5MJX.js.map +0 -1
- package/dist/figma-bridge-protocol.d.ts +0 -27
- package/dist/figma-bridge-protocol.js +0 -14
- package/dist/figma-bridge-protocol.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
isAllowedMethod,
|
|
4
|
-
normalizeBridgeMethod
|
|
5
|
-
} from "./chunk-CE6J5MJX.js";
|
|
6
2
|
|
|
7
3
|
// src/index.ts
|
|
8
4
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -222,7 +218,7 @@ function detectGovernanceChangesByFoundation(cached, fresh) {
|
|
|
222
218
|
return changes;
|
|
223
219
|
}
|
|
224
220
|
async function fetchDesignSystem(options) {
|
|
225
|
-
const { dsId: dsId2, apiKey: apiKey2, accessToken: accessToken2, apiBase: apiBase2 = "https://
|
|
221
|
+
const { dsId: dsId2, apiKey: apiKey2, accessToken: accessToken2, apiBase: apiBase2 = "https://atomix.studio", etag, forceRefresh = false } = options;
|
|
226
222
|
if (!dsId2) {
|
|
227
223
|
throw new Error("Missing dsId. Usage: fetchDesignSystem({ dsId: '...' })");
|
|
228
224
|
}
|
|
@@ -1093,9 +1089,7 @@ function formatSyncResponse(options) {
|
|
|
1093
1089
|
`;
|
|
1094
1090
|
});
|
|
1095
1091
|
response += `
|
|
1096
|
-
These changes are reflected in
|
|
1097
|
-
`;
|
|
1098
|
-
response += `Review the updated rules files to see the new guidance.
|
|
1092
|
+
These changes are reflected in the design system governance; run /--sync to update the skill and call getRules for the latest rules.
|
|
1099
1093
|
`;
|
|
1100
1094
|
}
|
|
1101
1095
|
if (diff) {
|
|
@@ -1218,886 +1212,9 @@ function getTokenStats(data) {
|
|
|
1218
1212
|
};
|
|
1219
1213
|
}
|
|
1220
1214
|
|
|
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
1215
|
// src/index.ts
|
|
1925
1216
|
import * as path from "path";
|
|
1926
1217
|
import * as fs from "fs";
|
|
1927
|
-
import { execSync } from "child_process";
|
|
1928
|
-
import { platform } from "os";
|
|
1929
|
-
import WebSocket, { WebSocketServer } from "ws";
|
|
1930
|
-
var FIGMA_BRIDGE_PORT = Number(process.env.FIGMA_BRIDGE_PORT) || 8765;
|
|
1931
|
-
var FIGMA_BRIDGE_HOST = process.env.FIGMA_BRIDGE_HOST || "127.0.0.1";
|
|
1932
|
-
var FIGMA_BRIDGE_TIMEOUT_MS = 15e3;
|
|
1933
|
-
var FIGMA_BRIDGE_TOKEN = process.env.FIGMA_BRIDGE_TOKEN || null;
|
|
1934
|
-
var FIGMA_CONNECTION_INSTRUCTIONS = {
|
|
1935
|
-
installAndRun: "In Figma: Open Plugins and run the Atomix plugin (Atomix Token Extractor). If it's not installed yet, install it from the Figma Community or your team's plugin library, then run it.",
|
|
1936
|
-
connect: 'In the plugin UI, tap **Connect** and wait until the status shows "Connected".',
|
|
1937
|
-
startBridge: "The Figma bridge runs with this MCP server. Ensure your AI environment has this MCP server running (e.g. in MCP settings), then in Figma run the Atomix plugin and tap Connect."
|
|
1938
|
-
};
|
|
1939
|
-
var bridgeWss = null;
|
|
1940
|
-
var pluginWs = null;
|
|
1941
|
-
var pendingBridgeRequests = /* @__PURE__ */ new Map();
|
|
1942
|
-
function ensureFigmaBridgePortFree(port) {
|
|
1943
|
-
const portStr = String(port);
|
|
1944
|
-
const ourPid = String(process.pid);
|
|
1945
|
-
try {
|
|
1946
|
-
if (platform() === "win32") {
|
|
1947
|
-
const out = execSync(`netstat -ano`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
1948
|
-
const pids = /* @__PURE__ */ new Set();
|
|
1949
|
-
for (const line of out.split(/\r?\n/)) {
|
|
1950
|
-
if (line.includes(`:${portStr}`) && line.includes("LISTENING")) {
|
|
1951
|
-
const parts = line.trim().split(/\s+/);
|
|
1952
|
-
const pid = parts[parts.length - 1];
|
|
1953
|
-
if (/^\d+$/.test(pid) && pid !== ourPid) pids.add(pid);
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
for (const pid of pids) {
|
|
1957
|
-
try {
|
|
1958
|
-
execSync(`taskkill /PID ${pid} /F`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
1959
|
-
console.error(`[atomix-mcp] Freed Figma bridge port ${port} (killed PID ${pid})`);
|
|
1960
|
-
} catch (_) {
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
} else {
|
|
1964
|
-
const out = execSync(`lsof -ti :${portStr}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1965
|
-
if (!out) return;
|
|
1966
|
-
const pids = out.split(/\s+/).filter((p) => p && p !== ourPid);
|
|
1967
|
-
for (const pid of pids) {
|
|
1968
|
-
try {
|
|
1969
|
-
execSync(`kill -9 ${pid}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
1970
|
-
console.error(`[atomix-mcp] Freed Figma bridge port ${port} (killed PID ${pid})`);
|
|
1971
|
-
} catch (_) {
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
} catch {
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
function startFigmaBridge() {
|
|
1979
|
-
if (bridgeWss) return;
|
|
1980
|
-
try {
|
|
1981
|
-
ensureFigmaBridgePortFree(FIGMA_BRIDGE_PORT);
|
|
1982
|
-
bridgeWss = new WebSocketServer({
|
|
1983
|
-
host: FIGMA_BRIDGE_HOST,
|
|
1984
|
-
port: FIGMA_BRIDGE_PORT,
|
|
1985
|
-
clientTracking: true
|
|
1986
|
-
});
|
|
1987
|
-
bridgeWss.on("connection", (ws, req) => {
|
|
1988
|
-
const url = req.url || "";
|
|
1989
|
-
const params = new URLSearchParams(url.startsWith("/") ? url.slice(1) : url);
|
|
1990
|
-
const token = params.get("token");
|
|
1991
|
-
const role = params.get("role");
|
|
1992
|
-
if (FIGMA_BRIDGE_TOKEN && token !== FIGMA_BRIDGE_TOKEN) {
|
|
1993
|
-
ws.close(4003, "Invalid or missing bridge token");
|
|
1994
|
-
return;
|
|
1995
|
-
}
|
|
1996
|
-
if (role !== "plugin") {
|
|
1997
|
-
ws.close(4002, "Only role=plugin is accepted (bridge runs in MCP server)");
|
|
1998
|
-
return;
|
|
1999
|
-
}
|
|
2000
|
-
if (pluginWs) {
|
|
2001
|
-
try {
|
|
2002
|
-
pluginWs.close();
|
|
2003
|
-
} catch (_) {
|
|
2004
|
-
}
|
|
2005
|
-
pluginWs = null;
|
|
2006
|
-
}
|
|
2007
|
-
pluginWs = ws;
|
|
2008
|
-
ws.on("message", (raw) => {
|
|
2009
|
-
const text = typeof raw === "string" ? raw : raw.toString("utf8");
|
|
2010
|
-
let msg;
|
|
2011
|
-
try {
|
|
2012
|
-
msg = JSON.parse(text);
|
|
2013
|
-
} catch {
|
|
2014
|
-
return;
|
|
2015
|
-
}
|
|
2016
|
-
const parsed = msg;
|
|
2017
|
-
if (parsed?.type === "ping" && typeof parsed.id === "string") {
|
|
2018
|
-
try {
|
|
2019
|
-
ws.send(JSON.stringify({ type: "pong", id: parsed.id }));
|
|
2020
|
-
} catch (_) {
|
|
2021
|
-
}
|
|
2022
|
-
return;
|
|
2023
|
-
}
|
|
2024
|
-
if (typeof parsed.id === "string" && ("result" in parsed || "error" in parsed)) {
|
|
2025
|
-
const pending = pendingBridgeRequests.get(parsed.id);
|
|
2026
|
-
if (pending) {
|
|
2027
|
-
clearTimeout(pending.timeout);
|
|
2028
|
-
pendingBridgeRequests.delete(parsed.id);
|
|
2029
|
-
if (parsed.error) pending.reject(new Error(parsed.error));
|
|
2030
|
-
else pending.resolve(parsed.result);
|
|
2031
|
-
}
|
|
2032
|
-
}
|
|
2033
|
-
});
|
|
2034
|
-
ws.on("close", () => {
|
|
2035
|
-
if (pluginWs === ws) pluginWs = null;
|
|
2036
|
-
});
|
|
2037
|
-
ws.on("error", () => {
|
|
2038
|
-
if (pluginWs === ws) pluginWs = null;
|
|
2039
|
-
});
|
|
2040
|
-
});
|
|
2041
|
-
bridgeWss.on("listening", () => {
|
|
2042
|
-
console.error(`[atomix-mcp] Figma bridge listening on ws://${FIGMA_BRIDGE_HOST}:${FIGMA_BRIDGE_PORT} (local only)`);
|
|
2043
|
-
if (FIGMA_BRIDGE_TOKEN) {
|
|
2044
|
-
console.error("[atomix-mcp] Figma bridge token required (FIGMA_BRIDGE_TOKEN)");
|
|
2045
|
-
}
|
|
2046
|
-
});
|
|
2047
|
-
bridgeWss.on("error", (err) => {
|
|
2048
|
-
console.error("[atomix-mcp] Figma bridge server error:", err);
|
|
2049
|
-
});
|
|
2050
|
-
} catch (err) {
|
|
2051
|
-
console.error("[atomix-mcp] Failed to start Figma bridge:", err);
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
function closeFigmaBridge() {
|
|
2055
|
-
if (pluginWs) {
|
|
2056
|
-
try {
|
|
2057
|
-
pluginWs.close();
|
|
2058
|
-
} catch (_) {
|
|
2059
|
-
}
|
|
2060
|
-
pluginWs = null;
|
|
2061
|
-
}
|
|
2062
|
-
if (bridgeWss) {
|
|
2063
|
-
try {
|
|
2064
|
-
bridgeWss.close();
|
|
2065
|
-
} catch (_) {
|
|
2066
|
-
}
|
|
2067
|
-
bridgeWss = null;
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
function isBridgeReachable() {
|
|
2071
|
-
return Promise.resolve(!!(pluginWs && pluginWs.readyState === WebSocket.OPEN));
|
|
2072
|
-
}
|
|
2073
|
-
function sendBridgeRequest(method, params, timeoutMs = FIGMA_BRIDGE_TIMEOUT_MS) {
|
|
2074
|
-
const normalized = normalizeBridgeMethod(method);
|
|
2075
|
-
if (!isAllowedMethod(normalized)) {
|
|
2076
|
-
return Promise.reject(new Error(`Bridge method not allowed: ${method}`));
|
|
2077
|
-
}
|
|
2078
|
-
const ws = pluginWs;
|
|
2079
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
2080
|
-
return Promise.reject(
|
|
2081
|
-
new Error("Figma plugin not connected. Open Figma, run Atomix plugin, and tap Connect.")
|
|
2082
|
-
);
|
|
2083
|
-
}
|
|
2084
|
-
const id = `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
2085
|
-
return new Promise((resolve3, reject) => {
|
|
2086
|
-
const timeout = setTimeout(() => {
|
|
2087
|
-
if (pendingBridgeRequests.delete(id)) {
|
|
2088
|
-
reject(new Error("Figma bridge timeout. " + FIGMA_CONNECTION_INSTRUCTIONS.startBridge + " Then " + FIGMA_CONNECTION_INSTRUCTIONS.connect));
|
|
2089
|
-
}
|
|
2090
|
-
}, timeoutMs);
|
|
2091
|
-
pendingBridgeRequests.set(id, { resolve: resolve3, reject, timeout });
|
|
2092
|
-
try {
|
|
2093
|
-
ws.send(JSON.stringify({ id, method: normalized, params }));
|
|
2094
|
-
} catch (e) {
|
|
2095
|
-
pendingBridgeRequests.delete(id);
|
|
2096
|
-
clearTimeout(timeout);
|
|
2097
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
2098
|
-
}
|
|
2099
|
-
});
|
|
2100
|
-
}
|
|
2101
1218
|
function parseArgs() {
|
|
2102
1219
|
const args = process.argv.slice(2);
|
|
2103
1220
|
let dsId2 = null;
|
|
@@ -2124,7 +1241,7 @@ function parseArgs() {
|
|
|
2124
1241
|
var cliArgs = parseArgs();
|
|
2125
1242
|
var { dsId, apiKey, accessToken } = cliArgs;
|
|
2126
1243
|
var apiBase = cliArgs.apiBase || "https://atomix.studio";
|
|
2127
|
-
var MCP_VERSION = "1.0.
|
|
1244
|
+
var MCP_VERSION = "1.0.36";
|
|
2128
1245
|
var cachedData = null;
|
|
2129
1246
|
var cachedETag = null;
|
|
2130
1247
|
var cachedMcpTier = null;
|
|
@@ -2230,7 +1347,7 @@ async function fetchDesignSystemForMCP(forceRefresh = false) {
|
|
|
2230
1347
|
return result.data;
|
|
2231
1348
|
}
|
|
2232
1349
|
var TOKEN_CATEGORIES = ["colors", "typography", "spacing", "sizing", "shadows", "radius", "borders", "motion", "zIndex"];
|
|
2233
|
-
function
|
|
1350
|
+
function typesetKeyToFontFamilyRole(key) {
|
|
2234
1351
|
const prefix = (key.split("-")[0] ?? key).toLowerCase();
|
|
2235
1352
|
if (prefix === "display" || prefix.startsWith("display")) return "display";
|
|
2236
1353
|
if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
|
|
@@ -2250,7 +1367,7 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
|
|
|
2250
1367
|
const p = cssPrefix ? `${cssPrefix}-` : "";
|
|
2251
1368
|
const typesets = [];
|
|
2252
1369
|
for (const key of Object.keys(fontSize)) {
|
|
2253
|
-
const role =
|
|
1370
|
+
const role = typesetKeyToFontFamilyRole(key);
|
|
2254
1371
|
const familyName = fontFamily[role] ?? fontFamily.body;
|
|
2255
1372
|
const fontFamilyVarName = familyName ? `--${p}typography-font-family-${role}` : void 0;
|
|
2256
1373
|
const fontFamilyVar = familyName ? `var(${fontFamilyVarName})` : "";
|
|
@@ -2298,7 +1415,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2298
1415
|
if (cachedMcpTier === "pro") {
|
|
2299
1416
|
console.error("[Atomix MCP] Resolved tier = pro.");
|
|
2300
1417
|
} else if (cachedMcpTier === "free") {
|
|
2301
|
-
console.error("[Atomix MCP] Resolved tier = free.
|
|
1418
|
+
console.error("[Atomix MCP] Resolved tier = free.");
|
|
2302
1419
|
}
|
|
2303
1420
|
} catch (err) {
|
|
2304
1421
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2439,7 +1556,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2439
1556
|
},
|
|
2440
1557
|
{
|
|
2441
1558
|
name: "syncAll",
|
|
2442
|
-
description: "Sync tokens, AI rules,
|
|
1559
|
+
description: "Sync tokens, AI rules, skill (.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.",
|
|
2443
1560
|
inputSchema: {
|
|
2444
1561
|
type: "object",
|
|
2445
1562
|
properties: {
|
|
@@ -2495,15 +1612,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2495
1612
|
properties: {},
|
|
2496
1613
|
required: []
|
|
2497
1614
|
}
|
|
2498
|
-
},
|
|
2499
|
-
{
|
|
2500
|
-
name: "syncToFigma",
|
|
2501
|
-
description: "Push the owner's design system to Figma: creates color variable collection (Light/Dark), color and paint styles, number variables (spacing, radius, borders, sizing, breakpoints), text styles, and shadow effect styles. Uses local WebSocket bridge and Atomix Figma plugin (no Figma REST API). No arguments. If the bridge is not running, the response includes agentInstruction to start it; only if that fails should the user start the bridge and connect the plugin. Call this when the user asks to 'sync to Figma' or 'push DS to Figma'.",
|
|
2502
|
-
inputSchema: {
|
|
2503
|
-
type: "object",
|
|
2504
|
-
properties: {},
|
|
2505
|
-
required: []
|
|
2506
|
-
}
|
|
2507
1615
|
}
|
|
2508
1616
|
];
|
|
2509
1617
|
return { tools: toolsList };
|
|
@@ -2681,7 +1789,8 @@ Version: ${designSystemData.meta.version}`,
|
|
|
2681
1789
|
hasRefactorRecommendation: !!lastSyncAffectedTokens?.removed.length,
|
|
2682
1790
|
deprecatedTokenCount: lastSyncAffectedTokens?.removed.length || 0
|
|
2683
1791
|
});
|
|
2684
|
-
|
|
1792
|
+
const tokenSyncChangeSummary = fileExists && ["css", "scss", "less"].includes(format) ? { fileExisted: true, previousVarCount: existingTokens.size, currentVarCount: tokenCount } : { fileExisted: false, previousVarCount: void 0, currentVarCount: tokenCount };
|
|
1793
|
+
return { responseText: response, rulesResults, validation, tokenSyncChangeSummary };
|
|
2685
1794
|
}
|
|
2686
1795
|
switch (name) {
|
|
2687
1796
|
case "getToken": {
|
|
@@ -2853,7 +1962,7 @@ Version: ${designSystemData.meta.version}`,
|
|
|
2853
1962
|
const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
|
|
2854
1963
|
console.error(`[getRules] Fetching: ${rulesUrl}${topic ? ` topic=${topic}` : ""}`);
|
|
2855
1964
|
const headers = { "Content-Type": "application/json" };
|
|
2856
|
-
if (
|
|
1965
|
+
if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`;
|
|
2857
1966
|
try {
|
|
2858
1967
|
const response = await fetch(rulesUrl, { headers });
|
|
2859
1968
|
console.error(`[getRules] Response status: ${response.status}`);
|
|
@@ -3113,10 +2222,12 @@ Version: ${designSystemData.meta.version}`,
|
|
|
3113
2222
|
const parts = [dryRun ? "[DRY RUN] syncAll report (no files written)." : "\u2713 syncAll complete."];
|
|
3114
2223
|
let tokenResponseText = "";
|
|
3115
2224
|
const allValidation = [];
|
|
2225
|
+
let tokenSyncChangeSummary;
|
|
3116
2226
|
if (!skipTokens) {
|
|
3117
2227
|
const result = await performTokenSyncAndRules(data, output, format, dryRun, projectRoot);
|
|
3118
2228
|
tokenResponseText = result.responseText;
|
|
3119
2229
|
allValidation.push(...result.validation);
|
|
2230
|
+
tokenSyncChangeSummary = result.tokenSyncChangeSummary;
|
|
3120
2231
|
if (dryRun) {
|
|
3121
2232
|
parts.push(`Would write tokens: ${output} (${format})`);
|
|
3122
2233
|
} else {
|
|
@@ -3126,10 +2237,11 @@ Version: ${designSystemData.meta.version}`,
|
|
|
3126
2237
|
const dsVersion = String(data.meta.version ?? "1.0.0");
|
|
3127
2238
|
const dsExportedAt = data.meta.exportedAt;
|
|
3128
2239
|
const skillsDir = path.resolve(projectRoot, ".cursor/skills/atomix-ds");
|
|
3129
|
-
const skillPath1 = path.join(skillsDir, "SKILL.md");
|
|
3130
2240
|
const manifestPath = path.resolve(projectRoot, "atomix-dependencies.json");
|
|
2241
|
+
const dependencySkills = getSyncDependencySkills(dsVersion, dsExportedAt);
|
|
3131
2242
|
if (dryRun) {
|
|
3132
|
-
|
|
2243
|
+
const skillList = dependencySkills.map((s) => s.path).join(", ");
|
|
2244
|
+
parts.push(`Would write skills: ${skillList}`);
|
|
3133
2245
|
parts.push("Would write manifest: atomix-dependencies.json");
|
|
3134
2246
|
const reportText = [parts.join("\n"), tokenResponseText].filter(Boolean).join("\n\n---\n\n");
|
|
3135
2247
|
return {
|
|
@@ -3139,10 +2251,18 @@ ${reportText}` }]
|
|
|
3139
2251
|
};
|
|
3140
2252
|
}
|
|
3141
2253
|
if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
|
|
3142
|
-
const
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
2254
|
+
const skillsWritten = [];
|
|
2255
|
+
for (const sk of dependencySkills) {
|
|
2256
|
+
const absPath = path.resolve(projectRoot, sk.path);
|
|
2257
|
+
const existed = fs.existsSync(absPath);
|
|
2258
|
+
const prevVersion = existed ? readSkillVersionFromFile(absPath) : null;
|
|
2259
|
+
const fromLabel = existed ? prevVersion ? `DS v${prevVersion}` : "existing" : "missing";
|
|
2260
|
+
fs.writeFileSync(absPath, sk.content);
|
|
2261
|
+
allValidation.push({ path: absPath, status: fs.existsSync(absPath) ? "OK" : "FAIL", detail: "Written." });
|
|
2262
|
+
skillsWritten.push({ path: sk.path, shortName: sk.shortName, from: fromLabel });
|
|
2263
|
+
}
|
|
2264
|
+
const skillShortNames = skillsWritten.map((s) => s.shortName).join(" + ");
|
|
2265
|
+
parts.push(`Skills: ${skillShortNames} (DS v${dsVersion})`);
|
|
3146
2266
|
const tokens = data.tokens;
|
|
3147
2267
|
const typography = tokens?.typography;
|
|
3148
2268
|
const fontFamily = typography?.fontFamily;
|
|
@@ -3161,6 +2281,7 @@ ${reportText}` }]
|
|
|
3161
2281
|
};
|
|
3162
2282
|
const lib = icons?.library || "lucide";
|
|
3163
2283
|
const iconPkgs = ICON_PACKAGES[lib] || ICON_PACKAGES.lucide;
|
|
2284
|
+
const skillsManifestEntry = { skill: ".cursor/skills/atomix-ds/SKILL.md", syncedAtVersion: String(data.meta.version ?? "1.0.0") };
|
|
3164
2285
|
const manifest = {
|
|
3165
2286
|
designSystem: { name: data.meta.name, version: data.meta.version },
|
|
3166
2287
|
tokenFile: skipTokens ? void 0 : output,
|
|
@@ -3171,29 +2292,73 @@ ${reportText}` }]
|
|
|
3171
2292
|
strokeWidthValue: icons?.strokeWidth
|
|
3172
2293
|
},
|
|
3173
2294
|
fonts: { families: fontNames },
|
|
3174
|
-
skills:
|
|
3175
|
-
skill: ".cursor/skills/atomix-ds/SKILL.md",
|
|
3176
|
-
syncedAtVersion: data.meta.version ?? "1.0.0"
|
|
3177
|
-
}
|
|
2295
|
+
skills: skillsManifestEntry
|
|
3178
2296
|
};
|
|
2297
|
+
const manifestExisted = fs.existsSync(manifestPath);
|
|
3179
2298
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
3180
2299
|
allValidation.push({ path: manifestPath, status: fs.existsSync(manifestPath) ? "OK" : "FAIL", detail: "Written." });
|
|
3181
2300
|
parts.push("Manifest: atomix-dependencies.json (icons, fonts, skill paths)");
|
|
2301
|
+
const changeRows = [];
|
|
2302
|
+
if (!skipTokens && tokenSyncChangeSummary) {
|
|
2303
|
+
const fromStr = tokenSyncChangeSummary.fileExisted && tokenSyncChangeSummary.previousVarCount != null ? `${tokenSyncChangeSummary.previousVarCount} tokens in ${output}` : "missing";
|
|
2304
|
+
changeRows.push({
|
|
2305
|
+
what: "Tokens",
|
|
2306
|
+
from: fromStr,
|
|
2307
|
+
to: `${tokenSyncChangeSummary.currentVarCount} tokens in ${output} (${format})`
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
const skillsFrom = skillsWritten.length ? skillsWritten.every((s) => s.from === "missing") ? "missing" : "updated" : "";
|
|
2311
|
+
if (skillsWritten.length > 0) {
|
|
2312
|
+
changeRows.push({
|
|
2313
|
+
what: "Skills",
|
|
2314
|
+
from: skillsFrom,
|
|
2315
|
+
to: `${skillShortNames} (DS v${dsVersion})`
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
changeRows.push({
|
|
2319
|
+
what: "Manifest",
|
|
2320
|
+
from: manifestExisted ? "existing" : "missing",
|
|
2321
|
+
to: "atomix-dependencies.json"
|
|
2322
|
+
});
|
|
2323
|
+
if (lastSyncAffectedTokens && (lastSyncAffectedTokens.modified.length > 0 || lastSyncAffectedTokens.added.length > 0 || lastSyncAffectedTokens.removed.length > 0)) {
|
|
2324
|
+
const mod = lastSyncAffectedTokens.modified;
|
|
2325
|
+
const add = lastSyncAffectedTokens.added;
|
|
2326
|
+
const rem = lastSyncAffectedTokens.removed;
|
|
2327
|
+
const tokenNames = [
|
|
2328
|
+
...mod.map((m) => m.token),
|
|
2329
|
+
...add,
|
|
2330
|
+
...rem.map((r) => r.token)
|
|
2331
|
+
];
|
|
2332
|
+
const fromParts = [];
|
|
2333
|
+
if (mod.length) fromParts.push(`${mod.length} modified`);
|
|
2334
|
+
if (add.length) fromParts.push(`${add.length} added`);
|
|
2335
|
+
if (rem.length) fromParts.push(`${rem.length} removed`);
|
|
2336
|
+
const toDetail = tokenNames.length <= 8 ? tokenNames.join(", ") : `${tokenNames.length} tokens (${tokenNames.slice(0, 4).join(", ")}...)`;
|
|
2337
|
+
changeRows.push({
|
|
2338
|
+
what: "Token changes (this run)",
|
|
2339
|
+
from: fromParts.join(", ") || "\u2014",
|
|
2340
|
+
to: toDetail
|
|
2341
|
+
});
|
|
2342
|
+
if (mod.length > 0 && mod.length <= 15) {
|
|
2343
|
+
mod.forEach((m) => {
|
|
2344
|
+
changeRows.push({ what: ` ${m.token}`, from: m.oldValue, to: m.newValue });
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
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
2349
|
const summary = parts.join("\n");
|
|
3183
2350
|
const validationBlock = formatValidationBlock(allValidation);
|
|
3184
2351
|
const hasFailure = allValidation.some((e) => e.status === "FAIL");
|
|
3185
2352
|
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 ?
|
|
2353
|
+
const fullText = resultLine + changesList + "\n\n" + (tokenResponseText ? `---
|
|
3187
2354
|
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
|
|
2355
|
+
${tokenResponseText}` : "") + validationBlock;
|
|
3191
2356
|
return {
|
|
3192
2357
|
content: [{ type: "text", text: fullText }]
|
|
3193
2358
|
};
|
|
3194
2359
|
}
|
|
3195
2360
|
case "getDependencies": {
|
|
3196
|
-
const
|
|
2361
|
+
const platform = args?.platform;
|
|
3197
2362
|
const stack = args?.stack;
|
|
3198
2363
|
const tokens = data.tokens;
|
|
3199
2364
|
const typography = tokens?.typography;
|
|
@@ -3225,22 +2390,23 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
|
|
|
3225
2390
|
families: fontNames,
|
|
3226
2391
|
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
2392
|
},
|
|
3228
|
-
skill: {
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
2393
|
+
skill: (() => {
|
|
2394
|
+
const list = getSyncDependencySkills(String(data.meta.version ?? "1.0.0"), data.meta.exportedAt);
|
|
2395
|
+
const generic = list.find((s) => s.shortName === "SKILL.md");
|
|
2396
|
+
return generic ? { path: generic.path, content: GENERIC_SKILL_MD } : { path: ".cursor/skills/atomix-ds/SKILL.md", content: GENERIC_SKILL_MD };
|
|
2397
|
+
})(),
|
|
3232
2398
|
tokenFiles: {
|
|
3233
2399
|
files: ["tokens.css", "tokens.json"],
|
|
3234
2400
|
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."
|
|
3235
2401
|
},
|
|
3236
|
-
showcase:
|
|
2402
|
+
showcase: platform === "web" || !platform ? {
|
|
3237
2403
|
path: "atomix-setup-showcase.html",
|
|
3238
2404
|
template: SHOWCASE_HTML_TEMPLATE,
|
|
3239
2405
|
substitutionInstructions: 'The synced token file (from syncAll) always uses the --atmx- prefix for every CSS variable. Keep all var(--atmx-*) references in the template; do not remove or change the prefix. Replace placeholders with values from the synced token file. {{TOKENS_CSS_PATH}} = path to the synced token file (e.g. ./tokens.css). {{TYPESETS_LINK}} = if a typeset CSS file was created, the full <link rel=\\"stylesheet\\" href=\\"typesets.css\\"> tag, otherwise empty string. {{DS_NAME}} = design system name. {{HEADING_FONT_VAR}} = var(--atmx-typography-font-family-heading) or var(--atmx-typography-font-family-display). {{FONT_FAMILY_VAR}} = var(--atmx-typography-font-family-body). {{LARGEST_DISPLAY_TYPESET_CLASS}} = largest display typeset class from listTypesets (display role, largest font size; e.g. typeset-display-2xl), or empty string if no typeset file. {{LARGEST_BODY_TYPESET_CLASS}} = largest body typeset class from listTypesets (body role, largest font size; e.g. typeset-body-lg), or empty string if no typeset file. {{BODY_TYPESET_CLASS}} = default body typeset class from listTypesets (e.g. typeset-body-md), or empty string. {{FONT_LINK_TAG}} = Google Fonts <link> for the font, or empty string. {{BRAND_PRIMARY_VAR}} = var(--atmx-color-brand-primary). Icon on circle uses luminance of brand primary (script sets white or black); no semantic foreground var. {{BUTTON_PADDING_VAR}} = var(--atmx-spacing-scale-md) or closest spacing token for button padding. {{BUTTON_HEIGHT_VAR}} = var(--atmx-sizing-height-md) or closest height token. {{BUTTON_RADIUS_VAR}} = var(--atmx-radius-scale-md) or var(--atmx-radius-scale-lg). {{CIRCLE_PADDING_VAR}} = var(--atmx-spacing-scale-md) or var(--atmx-spacing-scale-sm) for icon circle padding. {{ICON_SIZE_VAR}} = var(--atmx-sizing-icon-md) or var(--atmx-sizing-icon-lg). {{CHECK_ICON_SVG}} = inline SVG for Check icon from the design system icon library (getDependencies.iconLibrary.package: lucide-react, @heroicons/react, or phosphor-react). Use 24x24 viewBox; stroke=\\"currentColor\\" for Lucide/Heroicons, fill=\\"currentColor\\" for Phosphor so the script can set icon color by luminance. If unavailable, use: <svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"2\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><path d=\\"M20 6L9 17l-5-5\\"/></svg>. Do not invent CSS variable names; use only vars that exist in the export.'
|
|
3240
2406
|
} : void 0,
|
|
3241
2407
|
meta: {
|
|
3242
2408
|
dsName: data.meta.name,
|
|
3243
|
-
platform:
|
|
2409
|
+
platform: platform ?? void 0,
|
|
3244
2410
|
stack: stack ?? void 0,
|
|
3245
2411
|
designSystemVersion: data.meta.version ?? "1.0.0",
|
|
3246
2412
|
designSystemExportedAt: data.meta.exportedAt
|
|
@@ -3253,213 +2419,13 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
|
|
|
3253
2419
|
}]
|
|
3254
2420
|
};
|
|
3255
2421
|
}
|
|
3256
|
-
case "syncToFigma": {
|
|
3257
|
-
const payloads = buildFigmaPayloadsFromDS(data);
|
|
3258
|
-
const out = {};
|
|
3259
|
-
const agentStartBridge = `The Figma bridge runs with this MCP server. Ensure your AI environment has this MCP server configured and running, then in Figma run the Atomix plugin and tap Connect. Then call **syncToFigma** again.`;
|
|
3260
|
-
const userSteps = [
|
|
3261
|
-
FIGMA_CONNECTION_INSTRUCTIONS.startBridge,
|
|
3262
|
-
FIGMA_CONNECTION_INSTRUCTIONS.installAndRun,
|
|
3263
|
-
FIGMA_CONNECTION_INSTRUCTIONS.connect,
|
|
3264
|
-
"Then run Sync to Figma again."
|
|
3265
|
-
].join(" ");
|
|
3266
|
-
try {
|
|
3267
|
-
const reachable = await isBridgeReachable();
|
|
3268
|
-
if (!reachable) {
|
|
3269
|
-
out.bridgeNotRunning = true;
|
|
3270
|
-
out.agentInstruction = agentStartBridge;
|
|
3271
|
-
out.userInstruction = `If the bridge still does not connect: ${userSteps}`;
|
|
3272
|
-
out.figmaPayload = {
|
|
3273
|
-
colorVariableCollections: payloads.colorVariableCollections,
|
|
3274
|
-
paintStyles: payloads.paintStyles,
|
|
3275
|
-
textStyles: payloads.textStyles,
|
|
3276
|
-
numberVariableCollections: payloads.numberVariableCollections,
|
|
3277
|
-
effectStyles: payloads.effectStyles
|
|
3278
|
-
};
|
|
3279
|
-
return {
|
|
3280
|
-
content: [{ type: "text", text: JSON.stringify(out, null, 2) }]
|
|
3281
|
-
};
|
|
3282
|
-
}
|
|
3283
|
-
const colorResults = [];
|
|
3284
|
-
for (const coll of payloads.colorVariableCollections) {
|
|
3285
|
-
if (coll.variables.length > 0) {
|
|
3286
|
-
const result = await sendBridgeRequest("create_color_variables", {
|
|
3287
|
-
collectionName: coll.collectionName,
|
|
3288
|
-
modes: coll.modes,
|
|
3289
|
-
variables: coll.variables,
|
|
3290
|
-
removeVariablesNotInPayload: true,
|
|
3291
|
-
applyScopes: coll.applyScopes
|
|
3292
|
-
});
|
|
3293
|
-
colorResults.push({ collectionName: coll.collectionName, result });
|
|
3294
|
-
}
|
|
3295
|
-
}
|
|
3296
|
-
if (colorResults.length > 0) {
|
|
3297
|
-
out.colorVariables = colorResults.length === 1 ? colorResults[0].result : colorResults;
|
|
3298
|
-
}
|
|
3299
|
-
if (payloads.paintStyles.length > 0) {
|
|
3300
|
-
out.paintStyles = await sendBridgeRequest("create_paint_styles", {
|
|
3301
|
-
styles: payloads.paintStyles,
|
|
3302
|
-
removePaintStylesNotInPayload: true
|
|
3303
|
-
});
|
|
3304
|
-
}
|
|
3305
|
-
if (payloads.numberVariableCollections.length > 0) {
|
|
3306
|
-
const numberResults = [];
|
|
3307
|
-
try {
|
|
3308
|
-
for (const coll of payloads.numberVariableCollections) {
|
|
3309
|
-
const result = await sendBridgeRequest("create_number_variables", {
|
|
3310
|
-
collectionName: coll.collectionName,
|
|
3311
|
-
variables: coll.variables.map((v) => ({ name: v.name, value: v.value })),
|
|
3312
|
-
scopes: coll.scopes,
|
|
3313
|
-
removeVariablesNotInPayload: true
|
|
3314
|
-
});
|
|
3315
|
-
numberResults.push({ categoryKey: coll.categoryKey, result });
|
|
3316
|
-
}
|
|
3317
|
-
out.numberVariables = numberResults;
|
|
3318
|
-
} catch (e) {
|
|
3319
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
3320
|
-
out.numberVariables = { error: msg };
|
|
3321
|
-
if (msg.includes("Method not allowed") && msg.includes("create_number_variables")) {
|
|
3322
|
-
out.numberVariablesHint = "Number variables require the latest Atomix Figma plugin and bridge. Rebuild the plugin and bridge, reload the plugin in Figma, restart the bridge, then sync again.";
|
|
3323
|
-
}
|
|
3324
|
-
}
|
|
3325
|
-
}
|
|
3326
|
-
if (payloads.textStyles.length > 0) {
|
|
3327
|
-
out.textStyles = await sendBridgeRequest("create_text_styles", {
|
|
3328
|
-
styles: payloads.textStyles,
|
|
3329
|
-
removeTextStylesNotInPayload: true
|
|
3330
|
-
});
|
|
3331
|
-
}
|
|
3332
|
-
if (payloads.effectStyles.length > 0) {
|
|
3333
|
-
out.effectStyles = await sendBridgeRequest("create_effect_styles", {
|
|
3334
|
-
styles: payloads.effectStyles,
|
|
3335
|
-
removeShadowStylesNotInPayload: true
|
|
3336
|
-
});
|
|
3337
|
-
}
|
|
3338
|
-
out.figmaPayload = {
|
|
3339
|
-
colorVariableCollections: payloads.colorVariableCollections,
|
|
3340
|
-
paintStyles: payloads.paintStyles,
|
|
3341
|
-
textStyles: payloads.textStyles,
|
|
3342
|
-
numberVariableCollections: payloads.numberVariableCollections,
|
|
3343
|
-
effectStyles: payloads.effectStyles
|
|
3344
|
-
};
|
|
3345
|
-
} catch (e) {
|
|
3346
|
-
out.error = e instanceof Error ? e.message : String(e);
|
|
3347
|
-
out.figmaPayload = {
|
|
3348
|
-
colorVariableCollections: payloads.colorVariableCollections,
|
|
3349
|
-
paintStyles: payloads.paintStyles,
|
|
3350
|
-
textStyles: payloads.textStyles,
|
|
3351
|
-
numberVariableCollections: payloads.numberVariableCollections,
|
|
3352
|
-
effectStyles: payloads.effectStyles
|
|
3353
|
-
};
|
|
3354
|
-
const errMsg = out.error.toLowerCase();
|
|
3355
|
-
const connectionFailure = errMsg.includes("econnrefused") || errMsg.includes("bridge timeout") || errMsg.includes("websocket") || errMsg.includes("network");
|
|
3356
|
-
if (connectionFailure) {
|
|
3357
|
-
out.bridgeNotRunning = true;
|
|
3358
|
-
out.agentInstruction = agentStartBridge;
|
|
3359
|
-
out.userInstruction = `If the bridge still does not connect: ${userSteps}`;
|
|
3360
|
-
} else if (errMsg.includes("plugin not connected") || errMsg.includes("figma plugin")) {
|
|
3361
|
-
out.userInstruction = `${FIGMA_CONNECTION_INSTRUCTIONS.installAndRun} ${FIGMA_CONNECTION_INSTRUCTIONS.connect}`;
|
|
3362
|
-
}
|
|
3363
|
-
}
|
|
3364
|
-
const textStylesResult = out.textStyles;
|
|
3365
|
-
if (textStylesResult?.failed && textStylesResult.failures?.length) {
|
|
3366
|
-
const firstReason = textStylesResult.failures[0].reason;
|
|
3367
|
-
out.summary = `Text styles: ${textStylesResult.failed} could not be created. ${firstReason}`;
|
|
3368
|
-
}
|
|
3369
|
-
if (out.numberVariablesHint) {
|
|
3370
|
-
out.summary = [out.summary, out.numberVariablesHint].filter(Boolean).join(" ");
|
|
3371
|
-
}
|
|
3372
|
-
const summaryParts = [];
|
|
3373
|
-
const colorResult = out.colorVariables;
|
|
3374
|
-
if (colorResult) {
|
|
3375
|
-
const results = Array.isArray(colorResult) ? colorResult : [{ result: colorResult }];
|
|
3376
|
-
let totalSynced = 0;
|
|
3377
|
-
let totalRemoved = 0;
|
|
3378
|
-
for (const r of results) {
|
|
3379
|
-
const res = r.result;
|
|
3380
|
-
if (res?.variableNames?.length) totalSynced += res.variableNames.length;
|
|
3381
|
-
if ((res?.removed ?? 0) > 0) totalRemoved += res.removed ?? 0;
|
|
3382
|
-
}
|
|
3383
|
-
if (totalSynced > 0 || totalRemoved > 0) {
|
|
3384
|
-
const parts = [];
|
|
3385
|
-
if (totalSynced > 0) parts.push(`${totalSynced} synced`);
|
|
3386
|
-
if (totalRemoved > 0) parts.push(`${totalRemoved} removed`);
|
|
3387
|
-
summaryParts.push(`Colors: ${parts.join(", ")}.`);
|
|
3388
|
-
}
|
|
3389
|
-
}
|
|
3390
|
-
const paintResult = out.paintStyles;
|
|
3391
|
-
if (paintResult) {
|
|
3392
|
-
const c = paintResult.created ?? 0;
|
|
3393
|
-
const u = paintResult.updated ?? 0;
|
|
3394
|
-
const r = paintResult.removed ?? 0;
|
|
3395
|
-
if (c + u + r > 0) {
|
|
3396
|
-
const parts = [];
|
|
3397
|
-
if (c > 0) parts.push(`${c} created`);
|
|
3398
|
-
if (u > 0) parts.push(`${u} updated`);
|
|
3399
|
-
if (r > 0) parts.push(`${r} removed`);
|
|
3400
|
-
summaryParts.push(`Paint styles: ${parts.join(", ")}.`);
|
|
3401
|
-
}
|
|
3402
|
-
}
|
|
3403
|
-
const effectResult = out.effectStyles;
|
|
3404
|
-
if (effectResult) {
|
|
3405
|
-
const c = effectResult.created ?? 0;
|
|
3406
|
-
const u = effectResult.updated ?? 0;
|
|
3407
|
-
const r = effectResult.removed ?? 0;
|
|
3408
|
-
if (c + u + r > 0) {
|
|
3409
|
-
const parts = [];
|
|
3410
|
-
if (c > 0) parts.push(`${c} created`);
|
|
3411
|
-
if (u > 0) parts.push(`${u} updated`);
|
|
3412
|
-
if (r > 0) parts.push(`${r} removed`);
|
|
3413
|
-
summaryParts.push(`Effect styles (shadows): ${parts.join(", ")}.`);
|
|
3414
|
-
if (effectResult.removedNames?.length) {
|
|
3415
|
-
summaryParts.push(`Removed: ${effectResult.removedNames.join(", ")}.`);
|
|
3416
|
-
}
|
|
3417
|
-
}
|
|
3418
|
-
}
|
|
3419
|
-
const numResult = out.numberVariables;
|
|
3420
|
-
if (Array.isArray(numResult)) {
|
|
3421
|
-
const total = numResult.reduce((acc, r) => acc + (r.result?.variableNames?.length ?? 0), 0);
|
|
3422
|
-
const totalRemoved = numResult.reduce((acc, r) => acc + (r.result?.removed ?? 0), 0);
|
|
3423
|
-
if (total > 0 || totalRemoved > 0) {
|
|
3424
|
-
const parts = [];
|
|
3425
|
-
if (total > 0) parts.push(`${total} synced`);
|
|
3426
|
-
if (totalRemoved > 0) parts.push(`${totalRemoved} removed`);
|
|
3427
|
-
summaryParts.push(`Number variables: ${parts.join(", ")}.`);
|
|
3428
|
-
}
|
|
3429
|
-
}
|
|
3430
|
-
const textStylesWithRemoved = out.textStyles;
|
|
3431
|
-
if (textStylesWithRemoved && (textStylesWithRemoved.created ?? 0) + (textStylesWithRemoved.updated ?? 0) + (textStylesWithRemoved.removed ?? 0) > 0) {
|
|
3432
|
-
const c = textStylesWithRemoved.created ?? 0;
|
|
3433
|
-
const u = textStylesWithRemoved.updated ?? 0;
|
|
3434
|
-
const r = textStylesWithRemoved.removed ?? 0;
|
|
3435
|
-
const parts = [];
|
|
3436
|
-
if (c > 0) parts.push(`${c} created`);
|
|
3437
|
-
if (u > 0) parts.push(`${u} updated`);
|
|
3438
|
-
if (r > 0) parts.push(`${r} removed`);
|
|
3439
|
-
summaryParts.push(`Text styles: ${parts.join(", ")}.`);
|
|
3440
|
-
}
|
|
3441
|
-
if (summaryParts.length > 0 && !out.error) {
|
|
3442
|
-
out.summary = [out.summary, summaryParts.join(" ")].filter(Boolean).join(" ");
|
|
3443
|
-
}
|
|
3444
|
-
out.motionEasingNote = "Motion easing tokens are not synced as Figma styles; Figma has no reusable easing style. Easing is only used in prototype transitions (e.g. smart animate). Duration/easing remain available as number variables (duration) or in export JSON.";
|
|
3445
|
-
const responseText = out.summary ? `${out.summary}
|
|
3446
|
-
|
|
3447
|
-
${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
|
|
3448
|
-
return {
|
|
3449
|
-
content: [{
|
|
3450
|
-
type: "text",
|
|
3451
|
-
text: responseText
|
|
3452
|
-
}],
|
|
3453
|
-
...out.error ? { isError: true } : {}
|
|
3454
|
-
};
|
|
3455
|
-
}
|
|
3456
2422
|
default:
|
|
3457
2423
|
return {
|
|
3458
2424
|
content: [{
|
|
3459
2425
|
type: "text",
|
|
3460
2426
|
text: JSON.stringify({
|
|
3461
2427
|
error: `Unknown tool: ${name}`,
|
|
3462
|
-
availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "getMcpVersion"
|
|
2428
|
+
availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "getMcpVersion"]
|
|
3463
2429
|
}, null, 2)
|
|
3464
2430
|
}]
|
|
3465
2431
|
};
|
|
@@ -3501,6 +2467,26 @@ ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
|
|
|
3501
2467
|
};
|
|
3502
2468
|
}
|
|
3503
2469
|
});
|
|
2470
|
+
function getSyncDependencySkills(dsVersion, dsExportedAt) {
|
|
2471
|
+
const genericWithVersion = injectSkillVersion(GENERIC_SKILL_MD, dsVersion, dsExportedAt);
|
|
2472
|
+
return [
|
|
2473
|
+
{
|
|
2474
|
+
path: ".cursor/skills/atomix-ds/SKILL.md",
|
|
2475
|
+
content: genericWithVersion,
|
|
2476
|
+
shortName: "SKILL.md"
|
|
2477
|
+
}
|
|
2478
|
+
];
|
|
2479
|
+
}
|
|
2480
|
+
function readSkillVersionFromFile(filePath) {
|
|
2481
|
+
if (!fs.existsSync(filePath)) return null;
|
|
2482
|
+
try {
|
|
2483
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
2484
|
+
const match = raw.match(/atomixDsVersion:\s*["']([^"']+)["']/);
|
|
2485
|
+
return match ? match[1] : null;
|
|
2486
|
+
} catch {
|
|
2487
|
+
return null;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
3504
2490
|
function injectSkillVersion(content, version, exportedAt) {
|
|
3505
2491
|
const endOfFrontmatter = content.indexOf("\n---\n", 3);
|
|
3506
2492
|
if (endOfFrontmatter === -1) return content;
|
|
@@ -3593,7 +2579,7 @@ When a semantic token doesn't exist for your use case, use the closest primitive
|
|
|
3593
2579
|
|
|
3594
2580
|
### 4. Syncing tokens to a file
|
|
3595
2581
|
|
|
3596
|
-
\`syncAll({ output?, format?, skipTokens? })\` \u2014 writes tokens to a file (default \`./tokens.css\`),
|
|
2582
|
+
\`syncAll({ output?, format?, skipTokens? })\` \u2014 writes tokens to a file (default \`./tokens.css\`), skill (.cursor/skills/atomix-ds/SKILL.md), and manifest. Use \`skipTokens: true\` to only write skills and manifest.
|
|
3597
2583
|
|
|
3598
2584
|
## Workflow
|
|
3599
2585
|
|
|
@@ -3620,7 +2606,7 @@ If no token matches, call \`searchTokens\` to find the closest option. Never inv
|
|
|
3620
2606
|
- **Fetch first:** Always call getRules and/or listTokens before writing any styles, regardless of platform or framework.
|
|
3621
2607
|
- **Semantic over primitive:** Prefer tokens that describe purpose (\`text-primary\`, \`bg-surface\`) over tokens that describe appearance (\`neutral.900\`, \`white\`).
|
|
3622
2608
|
- **Icons:** Size via \`getToken("sizing.icon.sm")\`; stroke width via \`getToken("icons.strokeWidth")\` when the DS defines it.
|
|
3623
|
-
- **Typography:** Use
|
|
2609
|
+
- **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
2610
|
- **No guessing:** If a value is not in the rules or token list, call searchTokens or listTokens to find the closest match.
|
|
3625
2611
|
- **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
2612
|
- **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.
|
|
@@ -3852,7 +2838,7 @@ Get your DS ID and token from the Export modal or Settings \u2192 Regenerate Ato
|
|
|
3852
2838
|
const topicRaw = rulesMatch[1]?.toLowerCase().trim();
|
|
3853
2839
|
const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
|
|
3854
2840
|
const headers = { "Content-Type": "application/json" };
|
|
3855
|
-
if (
|
|
2841
|
+
if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`;
|
|
3856
2842
|
const response = await fetch(rulesUrl, { headers });
|
|
3857
2843
|
if (!response.ok) throw new Error(`Failed to fetch rules: ${response.status}`);
|
|
3858
2844
|
const payload = await response.json();
|
|
@@ -3909,8 +2895,7 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
|
3909
2895
|
{ name: "--get-started", description: "Get started with design system in project. Three phases: scan, report and ask, then create only after you approve." },
|
|
3910
2896
|
{ name: "--rules", description: "Get design system governance rules (optionally by topic: colors, typo, motion, icons, layout, visual)." },
|
|
3911
2897
|
{ name: "--sync", description: "Sync tokens, AI rules, skills files, and dependencies manifest (icons, fonts). Use /--refactor to migrate deprecated tokens." },
|
|
3912
|
-
{ name: "--refactor", description: "Migrate deprecated tokens in codebase. Run after /--sync." }
|
|
3913
|
-
{ name: "--sync-to-figma", description: "Push this design system to Figma (variables, color + typography styles). Uses local bridge + plugin; no Figma token." }
|
|
2898
|
+
{ name: "--refactor", description: "Migrate deprecated tokens in codebase. Run after /--sync." }
|
|
3914
2899
|
];
|
|
3915
2900
|
return { prompts };
|
|
3916
2901
|
});
|
|
@@ -3928,7 +2913,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
3928
2913
|
}]
|
|
3929
2914
|
};
|
|
3930
2915
|
}
|
|
3931
|
-
const canonicalName = name === "--hello" ? "hello" : name === "--get-started" ? "atomix-setup" : name === "--rules" ? "design-system-rules" : name === "--sync" ? "sync" : name === "--refactor" ? "refactor" : name
|
|
2916
|
+
const canonicalName = name === "--hello" ? "hello" : name === "--get-started" ? "atomix-setup" : name === "--rules" ? "design-system-rules" : name === "--sync" ? "sync" : name === "--refactor" ? "refactor" : name;
|
|
3932
2917
|
const shouldForceRefresh = [
|
|
3933
2918
|
"hello",
|
|
3934
2919
|
"atomix-setup",
|
|
@@ -3936,8 +2921,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
3936
2921
|
"design-system-rules",
|
|
3937
2922
|
// --rules
|
|
3938
2923
|
"sync",
|
|
3939
|
-
"refactor"
|
|
3940
|
-
"sync-to-figma"
|
|
2924
|
+
"refactor"
|
|
3941
2925
|
].includes(canonicalName);
|
|
3942
2926
|
let data = null;
|
|
3943
2927
|
let stats = null;
|
|
@@ -4084,7 +3068,7 @@ ${welcome}`;
|
|
|
4084
3068
|
const topic = args?.topic?.toLowerCase().trim();
|
|
4085
3069
|
const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
|
|
4086
3070
|
const headers = { "Content-Type": "application/json" };
|
|
4087
|
-
if (
|
|
3071
|
+
if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`;
|
|
4088
3072
|
const response = await fetch(rulesUrl, { headers });
|
|
4089
3073
|
if (!response.ok) throw new Error(`Failed to fetch rules: ${response.status}`);
|
|
4090
3074
|
const payload = await response.json();
|
|
@@ -4283,20 +3267,6 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
|
|
|
4283
3267
|
]
|
|
4284
3268
|
});
|
|
4285
3269
|
}
|
|
4286
|
-
case "sync-to-figma": {
|
|
4287
|
-
return withMcpNotice({
|
|
4288
|
-
description: "Push design system to Figma via MCP tool",
|
|
4289
|
-
messages: [
|
|
4290
|
-
{
|
|
4291
|
-
role: "user",
|
|
4292
|
-
content: {
|
|
4293
|
-
type: "text",
|
|
4294
|
-
text: `Call the MCP tool **syncToFigma** now (no arguments). It pushes the design system into the open Figma file via the built-in bridge and Atomix plugin. Do not use the Figma REST API or external scripts. If the response includes \`bridgeNotRunning\` and \`agentInstruction\`, ensure your AI environment has this MCP server running, then in Figma run the Atomix plugin and tap Connect, then call syncToFigma again. Only if that fails, tell the user: (1) Ensure this MCP server is configured and running in your AI tool. (2) In Figma, open and run the Atomix plugin, then tap **Connect**. (3) Run Sync to Figma again. After a successful sync, report the **summary** from the response (what was created, updated, or removed)\u2014do not say "we added the whole design system" when only some items changed.`
|
|
4295
|
-
}
|
|
4296
|
-
}
|
|
4297
|
-
]
|
|
4298
|
-
});
|
|
4299
|
-
}
|
|
4300
3270
|
case "refactor": {
|
|
4301
3271
|
const refactorOutput = args?.output || "./tokens.css";
|
|
4302
3272
|
const refactorFormat = args?.format || "css";
|
|
@@ -4444,7 +3414,7 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
|
|
|
4444
3414
|
- 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
3415
|
- Call **getDependencies** with \`platform\` and optional \`stack\`. If it fails, tell the user the design system could not be reached and stop.
|
|
4446
3416
|
- 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, skill (
|
|
3417
|
+
- Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links, skill (SKILL.md), token files; for web, also include the **showcase page** (atomix-setup-showcase.html) if getDependencies returned a \`showcase\` object.
|
|
4448
3418
|
- Do not write, create, or add anything in Phase 1.
|
|
4449
3419
|
|
|
4450
3420
|
## Phase 2 \u2013 Report and ask
|
|
@@ -4457,8 +3427,8 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
|
|
|
4457
3427
|
|
|
4458
3428
|
- Run only when the user has said yes (all or specific items).
|
|
4459
3429
|
- For each approved item:
|
|
4460
|
-
- **Skill:** Prefer calling **syncAll** (it writes the skill into the repo). If writing
|
|
4461
|
-
- **Token file and
|
|
3430
|
+
- **Skill:** Prefer calling **syncAll** (it writes the skill into the repo). If writing manually, write getDependencies \`skill.content\` to \`skill.path\` under the project root.
|
|
3431
|
+
- **Token file and skill 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.
|
|
4462
3432
|
- **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
3433
|
- **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
3434
|
- **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).
|
|
@@ -4543,7 +3513,6 @@ ${tokenSummary}
|
|
|
4543
3513
|
| **/--get-started** | Get started with design system in project. Three phases; creates files only after you approve. |
|
|
4544
3514
|
| **/--rules** | Governance rules for your AI tool (e.g. Cursor, Copilot, Windsurf). |
|
|
4545
3515
|
| **/--sync** | Sync tokens, rules, skills, and dependencies manifest (icons, fonts). Safe: adds new, updates existing, marks deprecated. |
|
|
4546
|
-
| **/--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. |
|
|
4547
3516
|
| **/--refactor** | Migrate deprecated tokens in codebase. Run after /--sync. |
|
|
4548
3517
|
|
|
4549
3518
|
**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.
|
|
@@ -4568,7 +3537,6 @@ async function startServer() {
|
|
|
4568
3537
|
console.error("");
|
|
4569
3538
|
process.exit(1);
|
|
4570
3539
|
}
|
|
4571
|
-
startFigmaBridge();
|
|
4572
3540
|
const transport = new StdioServerTransport();
|
|
4573
3541
|
await server.connect(transport);
|
|
4574
3542
|
console.error(`Atomix MCP Server started for design system: ${dsId}`);
|
|
@@ -4578,7 +3546,6 @@ async function startServer() {
|
|
|
4578
3546
|
);
|
|
4579
3547
|
}
|
|
4580
3548
|
function onShutdown() {
|
|
4581
|
-
closeFigmaBridge();
|
|
4582
3549
|
}
|
|
4583
3550
|
process.on("SIGINT", onShutdown);
|
|
4584
3551
|
process.on("SIGTERM", onShutdown);
|