@atomixstudio/mcp 1.0.33 → 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 +11 -3
- 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 +1020 -1006
- 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";
|
|
@@ -17,8 +25,6 @@ import {
|
|
|
17
25
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
18
26
|
|
|
19
27
|
// ../atomix-sync-core/dist/index.js
|
|
20
|
-
import * as fs from "fs";
|
|
21
|
-
import * as path from "path";
|
|
22
28
|
import * as path3 from "path";
|
|
23
29
|
function generateETag(meta) {
|
|
24
30
|
const hash = `${meta.version}-${meta.updatedAt}`;
|
|
@@ -224,7 +230,7 @@ function detectGovernanceChangesByFoundation(cached, fresh) {
|
|
|
224
230
|
return changes;
|
|
225
231
|
}
|
|
226
232
|
async function fetchDesignSystem(options) {
|
|
227
|
-
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;
|
|
228
234
|
if (!dsId2) {
|
|
229
235
|
throw new Error("Missing dsId. Usage: fetchDesignSystem({ dsId: '...' })");
|
|
230
236
|
}
|
|
@@ -337,74 +343,6 @@ function diffTokens(oldContent, newCssVars, format, newDarkVars) {
|
|
|
337
343
|
}
|
|
338
344
|
return { added, modified, removed, addedDark, modifiedDark, removedDark };
|
|
339
345
|
}
|
|
340
|
-
async function syncRulesFiles(options) {
|
|
341
|
-
const { dsId: dsId2, apiKey: apiKey2, apiBase: apiBase2 = "https://atomixstudio.eu", rulesDir = process.cwd() } = options;
|
|
342
|
-
const rulesDirResolved = path.resolve(process.cwd(), rulesDir);
|
|
343
|
-
const toolsToSync = [
|
|
344
|
-
{ tool: "cursor", filename: ".cursorrules" },
|
|
345
|
-
{ tool: "windsurf", filename: ".windsurfrules" },
|
|
346
|
-
{ tool: "cline", filename: ".clinerules" },
|
|
347
|
-
{ tool: "continue", filename: ".continuerules" },
|
|
348
|
-
{ tool: "copilot", filename: "copilot-instructions.md", dir: ".github" },
|
|
349
|
-
{ tool: "generic", filename: "AI_GUIDELINES.md" }
|
|
350
|
-
];
|
|
351
|
-
const existingTools = toolsToSync.filter((t) => {
|
|
352
|
-
const filePath = t.dir ? path.join(rulesDirResolved, t.dir, t.filename) : path.join(rulesDirResolved, t.filename);
|
|
353
|
-
return fs.existsSync(filePath);
|
|
354
|
-
});
|
|
355
|
-
const toolsToWrite = existingTools.length > 0 ? existingTools : [{ tool: "cursor", filename: ".cursorrules" }];
|
|
356
|
-
const results = [];
|
|
357
|
-
for (const { tool, filename, dir } of toolsToWrite) {
|
|
358
|
-
try {
|
|
359
|
-
const rulesUrl = `${apiBase2}/api/ds/${dsId2}/rules?format=${tool}`;
|
|
360
|
-
const headers = { "Content-Type": "application/json" };
|
|
361
|
-
if (apiKey2) headers["x-api-key"] = apiKey2;
|
|
362
|
-
const response = await fetch(rulesUrl, { headers });
|
|
363
|
-
if (!response.ok) {
|
|
364
|
-
results.push({
|
|
365
|
-
tool,
|
|
366
|
-
filename,
|
|
367
|
-
path: dir ? `${dir}/${filename}` : filename,
|
|
368
|
-
success: false,
|
|
369
|
-
error: `Failed to fetch ${tool} rules: ${response.status}`
|
|
370
|
-
});
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
const rulesData = await response.json();
|
|
374
|
-
if (!rulesData.content) {
|
|
375
|
-
results.push({
|
|
376
|
-
tool,
|
|
377
|
-
filename,
|
|
378
|
-
path: dir ? `${dir}/${filename}` : filename,
|
|
379
|
-
success: false,
|
|
380
|
-
error: `No content for ${tool} rules`
|
|
381
|
-
});
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
const targetDir = dir ? path.join(rulesDirResolved, dir) : rulesDirResolved;
|
|
385
|
-
if (!fs.existsSync(targetDir)) {
|
|
386
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
387
|
-
}
|
|
388
|
-
const filePath = path.join(targetDir, filename);
|
|
389
|
-
fs.writeFileSync(filePath, rulesData.content);
|
|
390
|
-
results.push({
|
|
391
|
-
tool,
|
|
392
|
-
filename,
|
|
393
|
-
path: dir ? `${dir}/${filename}` : filename,
|
|
394
|
-
success: true
|
|
395
|
-
});
|
|
396
|
-
} catch (error) {
|
|
397
|
-
results.push({
|
|
398
|
-
tool,
|
|
399
|
-
filename,
|
|
400
|
-
path: dir ? `${dir}/${filename}` : filename,
|
|
401
|
-
success: false,
|
|
402
|
-
error: error instanceof Error ? error.message : String(error)
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
return results;
|
|
407
|
-
}
|
|
408
346
|
function generateCSSOutput(cssVariables, darkModeColors, deprecatedTokens = /* @__PURE__ */ new Map()) {
|
|
409
347
|
const lines = [
|
|
410
348
|
"/* Atomix Design System Tokens",
|
|
@@ -1163,9 +1101,7 @@ function formatSyncResponse(options) {
|
|
|
1163
1101
|
`;
|
|
1164
1102
|
});
|
|
1165
1103
|
response += `
|
|
1166
|
-
These changes are reflected in
|
|
1167
|
-
`;
|
|
1168
|
-
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.
|
|
1169
1105
|
`;
|
|
1170
1106
|
}
|
|
1171
1107
|
if (diff) {
|
|
@@ -1288,712 +1224,9 @@ function getTokenStats(data) {
|
|
|
1288
1224
|
};
|
|
1289
1225
|
}
|
|
1290
1226
|
|
|
1291
|
-
// ../figma-tools/dist/index.js
|
|
1292
|
-
var LEGACY_GROUP_PREFIX = {
|
|
1293
|
-
background: "bg",
|
|
1294
|
-
text: "text",
|
|
1295
|
-
icon: "icon",
|
|
1296
|
-
border: "border",
|
|
1297
|
-
brand: "brand",
|
|
1298
|
-
action: "action",
|
|
1299
|
-
feedback: "feedback"
|
|
1300
|
-
};
|
|
1301
|
-
var RESERVED_COLOR_KEYS = /* @__PURE__ */ new Set(["customScales", "neutral"]);
|
|
1302
|
-
function getColorGroupOrder(storedColors) {
|
|
1303
|
-
if (Array.isArray(storedColors._groupOrder) && storedColors._groupOrder.length > 0) {
|
|
1304
|
-
return storedColors._groupOrder;
|
|
1305
|
-
}
|
|
1306
|
-
return Object.keys(storedColors).filter(
|
|
1307
|
-
(k) => !k.startsWith("_") && !RESERVED_COLOR_KEYS.has(k) && typeof storedColors[k] === "object"
|
|
1308
|
-
);
|
|
1309
|
-
}
|
|
1310
|
-
function getGroupPrefix(storedColors, groupName) {
|
|
1311
|
-
const custom = storedColors._groupPrefix;
|
|
1312
|
-
if (custom && typeof custom[groupName] === "string") return custom[groupName];
|
|
1313
|
-
return LEGACY_GROUP_PREFIX[groupName] ?? groupName;
|
|
1314
|
-
}
|
|
1315
|
-
function refScaleToFigmaDisplayName(scaleFromRef) {
|
|
1316
|
-
const s = scaleFromRef.trim();
|
|
1317
|
-
if (!s) return s;
|
|
1318
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1319
|
-
}
|
|
1320
|
-
function referenceToFigmaPrimitiveName(ref, primitiveNames) {
|
|
1321
|
-
if (!ref || typeof ref !== "string") return null;
|
|
1322
|
-
const r = ref.trim();
|
|
1323
|
-
const scaleStep = /^([a-zA-Z]+)\.(\d+|[a-z]+)$/.exec(r);
|
|
1324
|
-
if (scaleStep) {
|
|
1325
|
-
const [, scale, step] = scaleStep;
|
|
1326
|
-
const scaleDisplay = refScaleToFigmaDisplayName(scale);
|
|
1327
|
-
const name = `${scaleDisplay} / ${step}`;
|
|
1328
|
-
return primitiveNames.has(name) ? name : null;
|
|
1329
|
-
}
|
|
1330
|
-
const radixMatch = /^radix\.([a-zA-Z]+)\.(\d+)$/.exec(r);
|
|
1331
|
-
if (radixMatch) {
|
|
1332
|
-
const [, family, step] = radixMatch;
|
|
1333
|
-
const scaleName = refScaleToFigmaDisplayName(family);
|
|
1334
|
-
const name = `${scaleName} / ${step}`;
|
|
1335
|
-
return primitiveNames.has(name) ? name : null;
|
|
1336
|
-
}
|
|
1337
|
-
const oneOffMatch = /^(?:new|oneOff)\.(.+)$/.exec(r);
|
|
1338
|
-
if (oneOffMatch) {
|
|
1339
|
-
const name = `One-off / ${oneOffMatch[1]}`;
|
|
1340
|
-
return primitiveNames.has(name) ? name : null;
|
|
1341
|
-
}
|
|
1342
|
-
const whiteAlpha = /^whiteAlpha\.(.+)$/.exec(r);
|
|
1343
|
-
if (whiteAlpha) {
|
|
1344
|
-
const name = `WhiteAlpha / ${whiteAlpha[1]}`;
|
|
1345
|
-
return primitiveNames.has(name) ? name : null;
|
|
1346
|
-
}
|
|
1347
|
-
const blackAlpha = /^blackAlpha\.(.+)$/.exec(r);
|
|
1348
|
-
if (blackAlpha) {
|
|
1349
|
-
const name = `BlackAlpha / ${blackAlpha[1]}`;
|
|
1350
|
-
return primitiveNames.has(name) ? name : null;
|
|
1351
|
-
}
|
|
1352
|
-
return null;
|
|
1353
|
-
}
|
|
1354
|
-
function referenceToScaleName(ref) {
|
|
1355
|
-
if (!ref || typeof ref !== "string") return null;
|
|
1356
|
-
const r = ref.trim();
|
|
1357
|
-
const scaleStep = /^([a-zA-Z]+)\.(\d+|[a-z]+)$/.exec(r);
|
|
1358
|
-
if (scaleStep) return refScaleToFigmaDisplayName(scaleStep[1]);
|
|
1359
|
-
const radixMatch = /^radix\.([a-zA-Z]+)\.(\d+)$/.exec(r);
|
|
1360
|
-
if (radixMatch) return refScaleToFigmaDisplayName(radixMatch[1]);
|
|
1361
|
-
return null;
|
|
1362
|
-
}
|
|
1363
|
-
function getReferencedScaleNames(storedColors) {
|
|
1364
|
-
const names = /* @__PURE__ */ new Set();
|
|
1365
|
-
const groupOrder = getColorGroupOrder(storedColors);
|
|
1366
|
-
for (const groupName of groupOrder) {
|
|
1367
|
-
const group = storedColors[groupName];
|
|
1368
|
-
if (!group || typeof group !== "object") continue;
|
|
1369
|
-
const keys = Object.keys(group).filter((k) => !k.startsWith("_") && k !== "governance");
|
|
1370
|
-
for (const key of keys) {
|
|
1371
|
-
const value = group[key];
|
|
1372
|
-
if (value === void 0 || typeof value !== "object" || value === null) continue;
|
|
1373
|
-
const theme = value;
|
|
1374
|
-
const lightScale = theme.light?.reference ? referenceToScaleName(theme.light.reference) : null;
|
|
1375
|
-
const darkScale = theme.dark?.reference ? referenceToScaleName(theme.dark.reference) : null;
|
|
1376
|
-
if (lightScale) names.add(lightScale);
|
|
1377
|
-
if (darkScale) names.add(darkScale);
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
return names;
|
|
1381
|
-
}
|
|
1382
|
-
function getFullScaleFromStored(storedColors, scaleName) {
|
|
1383
|
-
const key = scaleName.toLowerCase();
|
|
1384
|
-
if (key === "neutral" || key === "gray") {
|
|
1385
|
-
const neutral = storedColors.neutral;
|
|
1386
|
-
if (!neutral?.steps || typeof neutral.steps !== "object") return null;
|
|
1387
|
-
const out = {};
|
|
1388
|
-
for (const [step, stepValue] of Object.entries(neutral.steps)) {
|
|
1389
|
-
const hex = stepValue?.hex;
|
|
1390
|
-
if (typeof hex === "string" && /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/i.test(hex)) {
|
|
1391
|
-
out[step] = hex.startsWith("#") ? hex : `#${hex}`;
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
return Object.keys(out).length > 0 ? out : null;
|
|
1395
|
-
}
|
|
1396
|
-
const customScales = storedColors.customScales;
|
|
1397
|
-
if (Array.isArray(customScales)) {
|
|
1398
|
-
const keyNorm = key.replace(/\s+/g, "");
|
|
1399
|
-
const scale = customScales.find(
|
|
1400
|
-
(s) => s.name?.toLowerCase() === key || s.name?.toLowerCase().replace(/\s+/g, "") === keyNorm
|
|
1401
|
-
);
|
|
1402
|
-
if (scale?.steps && typeof scale.steps === "object") {
|
|
1403
|
-
const out = {};
|
|
1404
|
-
for (const [step, stepValue] of Object.entries(scale.steps)) {
|
|
1405
|
-
const hex = stepValue?.hex;
|
|
1406
|
-
if (typeof hex === "string" && /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/i.test(hex)) {
|
|
1407
|
-
out[step] = hex.startsWith("#") ? hex : `#${hex}`;
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
if (Object.keys(out).length > 0) return out;
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
return null;
|
|
1414
|
-
}
|
|
1415
|
-
function buildSemanticRefMap(storedColors, primitiveNames) {
|
|
1416
|
-
const out = {};
|
|
1417
|
-
const groupOrder = getColorGroupOrder(storedColors);
|
|
1418
|
-
for (const groupName of groupOrder) {
|
|
1419
|
-
const group = storedColors[groupName];
|
|
1420
|
-
if (!group || typeof group !== "object") continue;
|
|
1421
|
-
const prefix = getGroupPrefix(storedColors, groupName);
|
|
1422
|
-
const rowOrder = Array.isArray(group._rowOrder) ? group._rowOrder : void 0;
|
|
1423
|
-
const keys = Array.isArray(rowOrder) ? rowOrder : Object.keys(group).filter((k) => !k.startsWith("_") && k !== "governance");
|
|
1424
|
-
const toKebab = prefix === "action" || prefix === "brand" || prefix === "feedback";
|
|
1425
|
-
for (const key of keys) {
|
|
1426
|
-
const value = group[key];
|
|
1427
|
-
if (value === void 0 || typeof value !== "object" || value === null) continue;
|
|
1428
|
-
const theme = value;
|
|
1429
|
-
let cssKey = key === "app" ? "page" : key;
|
|
1430
|
-
if (toKebab) {
|
|
1431
|
-
cssKey = String(key).replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1432
|
-
}
|
|
1433
|
-
const fullKey = `${prefix}-${cssKey}`;
|
|
1434
|
-
const lightRef = theme.light?.reference ? referenceToFigmaPrimitiveName(theme.light.reference, primitiveNames) : null;
|
|
1435
|
-
const darkRef = theme.dark?.reference ? referenceToFigmaPrimitiveName(theme.dark.reference, primitiveNames) : null;
|
|
1436
|
-
if (lightRef || darkRef) {
|
|
1437
|
-
out[fullKey] = {};
|
|
1438
|
-
if (lightRef) out[fullKey].Light = lightRef;
|
|
1439
|
-
if (darkRef) out[fullKey].Dark = darkRef;
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
return out;
|
|
1444
|
-
}
|
|
1445
|
-
function figmaColorNameWithGroup(key) {
|
|
1446
|
-
if (key.includes("/")) {
|
|
1447
|
-
const [group2, ...rest] = key.split("/");
|
|
1448
|
-
const name2 = rest.join("/").trim();
|
|
1449
|
-
if (!name2) return key;
|
|
1450
|
-
const groupDisplay2 = group2.charAt(0).toUpperCase() + group2.slice(1).toLowerCase();
|
|
1451
|
-
return `${groupDisplay2} / ${name2}`;
|
|
1452
|
-
}
|
|
1453
|
-
const firstDash = key.indexOf("-");
|
|
1454
|
-
if (firstDash <= 0) return key;
|
|
1455
|
-
const group = key.slice(0, firstDash);
|
|
1456
|
-
const name = key.slice(firstDash + 1);
|
|
1457
|
-
const groupDisplay = group.charAt(0).toUpperCase() + group.slice(1).toLowerCase();
|
|
1458
|
-
return `${groupDisplay} / ${name}`;
|
|
1459
|
-
}
|
|
1460
|
-
var FIGMA_SHADOW_ORDER = {
|
|
1461
|
-
none: 0,
|
|
1462
|
-
xs: 1,
|
|
1463
|
-
sm: 2,
|
|
1464
|
-
md: 3,
|
|
1465
|
-
lg: 4,
|
|
1466
|
-
xl: 5,
|
|
1467
|
-
"2xl": 6,
|
|
1468
|
-
focus: 7
|
|
1469
|
-
};
|
|
1470
|
-
function tokenValueToNumber(s) {
|
|
1471
|
-
if (typeof s !== "string" || !s.trim()) return 0;
|
|
1472
|
-
const t = s.trim();
|
|
1473
|
-
if (t.endsWith("rem")) {
|
|
1474
|
-
const n2 = parseFloat(t.replace(/rem$/, ""));
|
|
1475
|
-
return Number.isFinite(n2) ? Math.round(n2 * 16) : 0;
|
|
1476
|
-
}
|
|
1477
|
-
if (t.endsWith("px")) {
|
|
1478
|
-
const n2 = parseFloat(t.replace(/px$/, ""));
|
|
1479
|
-
return Number.isFinite(n2) ? Math.round(n2) : 0;
|
|
1480
|
-
}
|
|
1481
|
-
const n = parseFloat(t);
|
|
1482
|
-
return Number.isFinite(n) ? n : 0;
|
|
1483
|
-
}
|
|
1484
|
-
function parseBoxShadowToFigmaEffect(shadowStr) {
|
|
1485
|
-
const s = shadowStr.trim();
|
|
1486
|
-
if (!s || s.toLowerCase() === "none") return null;
|
|
1487
|
-
const parsePx = (x) => typeof x === "string" ? parseFloat(x.replace(/px$/i, "")) : NaN;
|
|
1488
|
-
const colorMatch = s.match(/(rgba?\s*\([^)]+\)|#[0-9A-Fa-f]{3,8})\s*$/i);
|
|
1489
|
-
const colorStr = colorMatch ? colorMatch[1].trim() : void 0;
|
|
1490
|
-
const rest = (colorMatch ? s.slice(0, colorMatch.index) : s).trim();
|
|
1491
|
-
const parts = rest ? rest.split(/\s+/) : [];
|
|
1492
|
-
if (parts.length < 3) return null;
|
|
1493
|
-
const offsetX = parsePx(parts[0]);
|
|
1494
|
-
const offsetY = parsePx(parts[1]);
|
|
1495
|
-
const blur = parsePx(parts[2]);
|
|
1496
|
-
let spread = 0;
|
|
1497
|
-
if (parts.length >= 4) spread = parsePx(parts[3]);
|
|
1498
|
-
let r = 0, g = 0, b = 0, a = 0.1;
|
|
1499
|
-
if (colorStr) {
|
|
1500
|
-
const rgbaMatch = colorStr.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i);
|
|
1501
|
-
if (rgbaMatch) {
|
|
1502
|
-
r = Number(rgbaMatch[1]) / 255;
|
|
1503
|
-
g = Number(rgbaMatch[2]) / 255;
|
|
1504
|
-
b = Number(rgbaMatch[3]) / 255;
|
|
1505
|
-
a = rgbaMatch[4] != null ? parseFloat(rgbaMatch[4]) : 1;
|
|
1506
|
-
} else {
|
|
1507
|
-
const hexMatch = colorStr.match(/^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/);
|
|
1508
|
-
if (hexMatch) {
|
|
1509
|
-
r = parseInt(hexMatch[1].slice(0, 2), 16) / 255;
|
|
1510
|
-
g = parseInt(hexMatch[1].slice(2, 4), 16) / 255;
|
|
1511
|
-
b = parseInt(hexMatch[1].slice(4, 6), 16) / 255;
|
|
1512
|
-
a = hexMatch[2] ? parseInt(hexMatch[2], 16) / 255 : 0.1;
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
if (!Number.isFinite(offsetX) || !Number.isFinite(offsetY) || !Number.isFinite(blur)) return null;
|
|
1517
|
-
return {
|
|
1518
|
-
type: "DROP_SHADOW",
|
|
1519
|
-
offset: { x: offsetX, y: offsetY },
|
|
1520
|
-
radius: Math.max(0, blur),
|
|
1521
|
-
spread: Number.isFinite(spread) ? spread : 0,
|
|
1522
|
-
color: { r, g, b, a },
|
|
1523
|
-
visible: true,
|
|
1524
|
-
blendMode: "NORMAL"
|
|
1525
|
-
};
|
|
1526
|
-
}
|
|
1527
|
-
function parseBoxShadowToFigmaEffects(shadowStr) {
|
|
1528
|
-
const s = (shadowStr || "").trim();
|
|
1529
|
-
if (!s || s.toLowerCase() === "none") return [];
|
|
1530
|
-
const out = [];
|
|
1531
|
-
const segments = s.split(/\s*,\s*/);
|
|
1532
|
-
for (const seg of segments) {
|
|
1533
|
-
const effect = parseBoxShadowToFigmaEffect(seg.trim());
|
|
1534
|
-
if (effect) out.push(effect);
|
|
1535
|
-
}
|
|
1536
|
-
return out;
|
|
1537
|
-
}
|
|
1538
|
-
function colorTo8DigitHex(color) {
|
|
1539
|
-
if (!color || typeof color !== "string") return null;
|
|
1540
|
-
const s = color.trim();
|
|
1541
|
-
const rgbaMatch = s.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i);
|
|
1542
|
-
if (rgbaMatch) {
|
|
1543
|
-
const r = Math.max(0, Math.min(255, parseInt(rgbaMatch[1], 10)));
|
|
1544
|
-
const g = Math.max(0, Math.min(255, parseInt(rgbaMatch[2], 10)));
|
|
1545
|
-
const b = Math.max(0, Math.min(255, parseInt(rgbaMatch[3], 10)));
|
|
1546
|
-
const a = rgbaMatch[4] != null ? Math.max(0, Math.min(1, parseFloat(rgbaMatch[4]))) : 1;
|
|
1547
|
-
const aByte = Math.round(a * 255);
|
|
1548
|
-
const hex = "#" + [r, g, b, aByte].map((n) => n.toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
1549
|
-
return hex;
|
|
1550
|
-
}
|
|
1551
|
-
const hexMatch = s.match(/^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/i);
|
|
1552
|
-
if (hexMatch) {
|
|
1553
|
-
const rgb = hexMatch[1];
|
|
1554
|
-
const aa = hexMatch[2] ?? "FF";
|
|
1555
|
-
return `#${rgb}${aa}`.toUpperCase();
|
|
1556
|
-
}
|
|
1557
|
-
return null;
|
|
1558
|
-
}
|
|
1559
|
-
function typesetKeyToFontFamilyRole(key) {
|
|
1560
|
-
const prefix = (key.split("-")[0] ?? key).toLowerCase();
|
|
1561
|
-
if (prefix === "display" || prefix.startsWith("display")) return "display";
|
|
1562
|
-
if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
|
|
1563
|
-
if (prefix === "mono" || prefix.startsWith("mono")) return "mono";
|
|
1564
|
-
if (prefix.startsWith("body")) return "body";
|
|
1565
|
-
return "body";
|
|
1566
|
-
}
|
|
1567
|
-
function buildFigmaPayloadsFromDS(data) {
|
|
1568
|
-
const tokens = data.tokens;
|
|
1569
|
-
const colors = tokens?.colors;
|
|
1570
|
-
const typography = tokens?.typography;
|
|
1571
|
-
const hexRe = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
|
|
1572
|
-
const modes = [];
|
|
1573
|
-
if (colors?.modes) {
|
|
1574
|
-
const light = colors.modes.light ?? {};
|
|
1575
|
-
const dark = colors.modes.dark ?? {};
|
|
1576
|
-
if (Object.keys(light).length > 0) modes.push("Light");
|
|
1577
|
-
if (Object.keys(dark).length > 0 && !modes.includes("Dark")) modes.push("Dark");
|
|
1578
|
-
}
|
|
1579
|
-
if (modes.length === 0) modes.push("Light");
|
|
1580
|
-
const primitiveModes = ["Value"];
|
|
1581
|
-
const dsName = data.meta?.name;
|
|
1582
|
-
const collectionPrefix = dsName ? `${dsName} ` : "";
|
|
1583
|
-
const referencedScaleNames = data.storedColors ? getReferencedScaleNames(data.storedColors) : /* @__PURE__ */ new Set();
|
|
1584
|
-
const primitiveVariables = [];
|
|
1585
|
-
const primitiveNames = /* @__PURE__ */ new Set();
|
|
1586
|
-
const oneOffHexToFigmaName = /* @__PURE__ */ new Map();
|
|
1587
|
-
const alphaScales = colors?.alphaScales;
|
|
1588
|
-
if (alphaScales?.whiteAlpha && typeof alphaScales.whiteAlpha === "object") {
|
|
1589
|
-
for (const [step, value] of Object.entries(alphaScales.whiteAlpha)) {
|
|
1590
|
-
if (typeof value !== "string") continue;
|
|
1591
|
-
const hex = colorTo8DigitHex(value);
|
|
1592
|
-
if (!hex || !hexRe.test(hex)) continue;
|
|
1593
|
-
const figmaName = `WhiteAlpha / ${step}`;
|
|
1594
|
-
if (primitiveNames.has(figmaName)) continue;
|
|
1595
|
-
primitiveNames.add(figmaName);
|
|
1596
|
-
const values = {};
|
|
1597
|
-
for (const m of primitiveModes) values[m] = hex;
|
|
1598
|
-
primitiveVariables.push({ name: figmaName, values });
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
if (alphaScales?.blackAlpha && typeof alphaScales.blackAlpha === "object") {
|
|
1602
|
-
for (const [step, value] of Object.entries(alphaScales.blackAlpha)) {
|
|
1603
|
-
if (typeof value !== "string") continue;
|
|
1604
|
-
const hex = colorTo8DigitHex(value);
|
|
1605
|
-
if (!hex || !hexRe.test(hex)) continue;
|
|
1606
|
-
const figmaName = `BlackAlpha / ${step}`;
|
|
1607
|
-
if (primitiveNames.has(figmaName)) continue;
|
|
1608
|
-
primitiveNames.add(figmaName);
|
|
1609
|
-
const values = {};
|
|
1610
|
-
for (const m of primitiveModes) values[m] = hex;
|
|
1611
|
-
primitiveVariables.push({ name: figmaName, values });
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
if (colors?.scales && typeof colors.scales === "object") {
|
|
1615
|
-
for (const [scaleName, steps] of Object.entries(colors.scales)) {
|
|
1616
|
-
if (!steps || typeof steps !== "object") continue;
|
|
1617
|
-
let groupDisplay = scaleName.charAt(0).toUpperCase() + scaleName.slice(1);
|
|
1618
|
-
if (scaleName.toLowerCase() === "neutral" && data.storedColors) {
|
|
1619
|
-
const neutral = data.storedColors.neutral;
|
|
1620
|
-
if (neutral?.baseHueFamily) {
|
|
1621
|
-
groupDisplay = refScaleToFigmaDisplayName(neutral.baseHueFamily);
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
for (const [step, hexVal] of Object.entries(steps)) {
|
|
1625
|
-
if (typeof hexVal !== "string") continue;
|
|
1626
|
-
const hex = colorTo8DigitHex(hexVal) ?? (hexRe.test(hexVal) ? hexVal : null);
|
|
1627
|
-
if (!hex || !hexRe.test(hex)) continue;
|
|
1628
|
-
const figmaName = `${groupDisplay} / ${step}`;
|
|
1629
|
-
if (primitiveNames.has(figmaName)) continue;
|
|
1630
|
-
primitiveNames.add(figmaName);
|
|
1631
|
-
const values = {};
|
|
1632
|
-
for (const m of primitiveModes) values[m] = hex;
|
|
1633
|
-
primitiveVariables.push({ name: figmaName, values });
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
const radixVariables = [];
|
|
1638
|
-
const radixCollectionName = `${collectionPrefix}Colors Radix`;
|
|
1639
|
-
for (const scaleName of referencedScaleNames) {
|
|
1640
|
-
if (primitiveNames.has(`${scaleName} / 50`) || primitiveNames.has(`${scaleName} / 100`)) continue;
|
|
1641
|
-
const fromStored = data.storedColors ? getFullScaleFromStored(data.storedColors, scaleName) : null;
|
|
1642
|
-
const scaleData = fromStored ?? data.getRadixScale?.(scaleName) ?? null;
|
|
1643
|
-
if (!scaleData) continue;
|
|
1644
|
-
for (const [step, hexVal] of Object.entries(scaleData)) {
|
|
1645
|
-
const figmaName = `${scaleName} / ${step}`;
|
|
1646
|
-
if (primitiveNames.has(figmaName)) continue;
|
|
1647
|
-
const hex = colorTo8DigitHex(hexVal) ?? (hexRe.test(hexVal) ? hexVal : null);
|
|
1648
|
-
if (!hex || !hexRe.test(hex)) continue;
|
|
1649
|
-
primitiveNames.add(figmaName);
|
|
1650
|
-
const values = {};
|
|
1651
|
-
for (const m of primitiveModes) values[m] = hex;
|
|
1652
|
-
primitiveVariables.push({ name: figmaName, values });
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
if (colors?.oneOffs && Array.isArray(colors.oneOffs)) {
|
|
1656
|
-
for (const oneOff of colors.oneOffs) {
|
|
1657
|
-
if (!oneOff || typeof oneOff !== "object" || typeof oneOff.hex !== "string") continue;
|
|
1658
|
-
const hex = colorTo8DigitHex(oneOff.hex) ?? (hexRe.test(oneOff.hex) ? oneOff.hex : null);
|
|
1659
|
-
if (!hex || !hexRe.test(hex)) continue;
|
|
1660
|
-
const name = typeof oneOff.name === "string" && oneOff.name ? oneOff.name : oneOff.id ?? "unnamed";
|
|
1661
|
-
const figmaName = `One-off / ${name}`;
|
|
1662
|
-
if (primitiveNames.has(figmaName)) continue;
|
|
1663
|
-
primitiveNames.add(figmaName);
|
|
1664
|
-
const values = {};
|
|
1665
|
-
for (const m of primitiveModes) values[m] = hex;
|
|
1666
|
-
primitiveVariables.push({ name: figmaName, values });
|
|
1667
|
-
const normalizedHex = hex.replace(/^#/, "").toUpperCase();
|
|
1668
|
-
const key8 = normalizedHex.length === 6 ? normalizedHex + "FF" : normalizedHex;
|
|
1669
|
-
oneOffHexToFigmaName.set(key8, figmaName);
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
const semanticRefMap = data.storedColors && primitiveNames.size > 0 ? buildSemanticRefMap(data.storedColors, primitiveNames) : {};
|
|
1673
|
-
const semanticVariables = [];
|
|
1674
|
-
const semanticNames = /* @__PURE__ */ new Set();
|
|
1675
|
-
const primitivesCollectionName = `${collectionPrefix}Colors Primitives`;
|
|
1676
|
-
if (colors?.modes) {
|
|
1677
|
-
const light = colors.modes.light ?? {};
|
|
1678
|
-
const dark = colors.modes.dark ?? {};
|
|
1679
|
-
const orderedKeys = [...Object.keys(light)];
|
|
1680
|
-
for (const k of Object.keys(dark)) {
|
|
1681
|
-
if (!orderedKeys.includes(k)) orderedKeys.push(k);
|
|
1682
|
-
}
|
|
1683
|
-
for (const key of orderedKeys) {
|
|
1684
|
-
const lightVal = light[key];
|
|
1685
|
-
const darkVal = dark[key];
|
|
1686
|
-
const lightHex = typeof lightVal === "string" ? colorTo8DigitHex(lightVal) ?? (hexRe.test(lightVal) ? lightVal : null) : null;
|
|
1687
|
-
if (lightHex && hexRe.test(lightHex)) {
|
|
1688
|
-
const figmaName = figmaColorNameWithGroup(key);
|
|
1689
|
-
if (semanticNames.has(figmaName)) continue;
|
|
1690
|
-
semanticNames.add(figmaName);
|
|
1691
|
-
const darkHex = typeof darkVal === "string" ? colorTo8DigitHex(darkVal) ?? (hexRe.test(darkVal) ? darkVal : null) : null;
|
|
1692
|
-
const refs = semanticRefMap[key];
|
|
1693
|
-
const values = {
|
|
1694
|
-
...modes.includes("Light") && { Light: lightHex },
|
|
1695
|
-
...modes.includes("Dark") && { Dark: darkHex && hexRe.test(darkHex) ? darkHex : lightHex }
|
|
1696
|
-
};
|
|
1697
|
-
const aliasByMode = {};
|
|
1698
|
-
for (const m of modes) {
|
|
1699
|
-
const aliasFromRef = m === "Light" ? refs?.Light : refs?.Dark;
|
|
1700
|
-
if (aliasFromRef && primitiveNames.has(aliasFromRef)) {
|
|
1701
|
-
aliasByMode[m] = aliasFromRef;
|
|
1702
|
-
continue;
|
|
1703
|
-
}
|
|
1704
|
-
const hexForMode = m === "Light" ? lightHex : darkHex && hexRe.test(darkHex) ? darkHex : lightHex;
|
|
1705
|
-
const norm = hexForMode.replace(/^#/, "").toUpperCase();
|
|
1706
|
-
const key8 = norm.length === 6 ? norm + "FF" : norm;
|
|
1707
|
-
const oneOffAlias = oneOffHexToFigmaName.get(key8);
|
|
1708
|
-
if (oneOffAlias) {
|
|
1709
|
-
aliasByMode[m] = oneOffAlias;
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
semanticVariables.push({
|
|
1713
|
-
name: figmaName,
|
|
1714
|
-
values,
|
|
1715
|
-
...Object.keys(aliasByMode).length > 0 ? { aliasByMode } : {}
|
|
1716
|
-
});
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
if (colors?.static?.brand && typeof colors.static.brand === "object") {
|
|
1721
|
-
for (const [key, hex] of Object.entries(colors.static.brand)) {
|
|
1722
|
-
if (typeof hex !== "string" || !hexRe.test(hex)) continue;
|
|
1723
|
-
const figmaName = figmaColorNameWithGroup(`brand/${key}`);
|
|
1724
|
-
if (semanticNames.has(figmaName)) continue;
|
|
1725
|
-
semanticNames.add(figmaName);
|
|
1726
|
-
const values = {};
|
|
1727
|
-
for (const m of modes) values[m] = hex;
|
|
1728
|
-
semanticVariables.push({ name: figmaName, values });
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
const colorVariableCollections = [];
|
|
1732
|
-
if (primitiveVariables.length > 0) {
|
|
1733
|
-
colorVariableCollections.push({
|
|
1734
|
-
collectionName: primitivesCollectionName,
|
|
1735
|
-
modes: primitiveModes,
|
|
1736
|
-
variables: primitiveVariables,
|
|
1737
|
-
applyScopes: false
|
|
1738
|
-
});
|
|
1739
|
-
}
|
|
1740
|
-
if (radixVariables.length > 0) {
|
|
1741
|
-
colorVariableCollections.push({
|
|
1742
|
-
collectionName: radixCollectionName,
|
|
1743
|
-
modes,
|
|
1744
|
-
variables: radixVariables,
|
|
1745
|
-
applyScopes: false
|
|
1746
|
-
});
|
|
1747
|
-
}
|
|
1748
|
-
if (semanticVariables.length > 0) {
|
|
1749
|
-
const primitiveCollections = [];
|
|
1750
|
-
if (primitiveVariables.length > 0) primitiveCollections.push(primitivesCollectionName);
|
|
1751
|
-
if (radixVariables.length > 0) primitiveCollections.push(radixCollectionName);
|
|
1752
|
-
colorVariableCollections.push({
|
|
1753
|
-
collectionName: `${collectionPrefix}Colors Semantic`,
|
|
1754
|
-
modes,
|
|
1755
|
-
variables: semanticVariables,
|
|
1756
|
-
applyScopes: true,
|
|
1757
|
-
...primitiveCollections.length > 1 ? { primitiveCollectionNames: primitiveCollections } : primitiveCollections.length === 1 ? { primitiveCollectionName: primitiveCollections[0] } : {}
|
|
1758
|
-
});
|
|
1759
|
-
}
|
|
1760
|
-
const paintStyles = [];
|
|
1761
|
-
const textStyles = [];
|
|
1762
|
-
const sizeToPx = (val, basePx = 16) => {
|
|
1763
|
-
if (typeof val === "number") return Math.round(val);
|
|
1764
|
-
const s = String(val).trim();
|
|
1765
|
-
const pxMatch = s.match(/^([\d.]+)\s*px$/i);
|
|
1766
|
-
if (pxMatch) return Math.round(parseFloat(pxMatch[1]));
|
|
1767
|
-
const remMatch = s.match(/^([\d.]+)\s*rem$/i);
|
|
1768
|
-
if (remMatch) return Math.round(parseFloat(remMatch[1]) * basePx);
|
|
1769
|
-
const n = parseFloat(s);
|
|
1770
|
-
if (Number.isFinite(n)) return n <= 0 ? basePx : n < 50 ? Math.round(n * basePx) : Math.round(n);
|
|
1771
|
-
return basePx;
|
|
1772
|
-
};
|
|
1773
|
-
const letterSpacingToPx = (val, fontSizePx) => {
|
|
1774
|
-
if (val === void 0 || val === null) return void 0;
|
|
1775
|
-
if (typeof val === "number") return Math.round(val);
|
|
1776
|
-
const s = String(val).trim();
|
|
1777
|
-
const pxMatch = s.match(/^([-\d.]+)\s*px$/i);
|
|
1778
|
-
if (pxMatch) return Math.round(parseFloat(pxMatch[1]));
|
|
1779
|
-
const emMatch = s.match(/^([-\d.]+)\s*em$/i);
|
|
1780
|
-
if (emMatch) return Math.round(parseFloat(emMatch[1]) * fontSizePx);
|
|
1781
|
-
const n = parseFloat(s);
|
|
1782
|
-
return Number.isFinite(n) ? Math.round(n) : void 0;
|
|
1783
|
-
};
|
|
1784
|
-
const firstFont = (obj) => {
|
|
1785
|
-
if (typeof obj === "string") {
|
|
1786
|
-
const primary = obj.split(",")[0].trim().replace(/^['"]|['"]$/g, "");
|
|
1787
|
-
return primary || "Inter";
|
|
1788
|
-
}
|
|
1789
|
-
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
1790
|
-
const v = obj.body ?? obj.heading ?? obj.display ?? Object.values(obj)[0];
|
|
1791
|
-
return firstFont(v);
|
|
1792
|
-
}
|
|
1793
|
-
return "Inter";
|
|
1794
|
-
};
|
|
1795
|
-
const toFontFamilyString = (val) => {
|
|
1796
|
-
if (typeof val === "string") {
|
|
1797
|
-
const s = val.trim().replace(/^["']|["']$/g, "");
|
|
1798
|
-
return s || "Inter";
|
|
1799
|
-
}
|
|
1800
|
-
return firstFont(val);
|
|
1801
|
-
};
|
|
1802
|
-
const fontFamilyMap = typography?.fontFamily ?? {};
|
|
1803
|
-
const defaultFontFamily = typography ? firstFont(typography.fontFamily ?? "Inter") : "Inter";
|
|
1804
|
-
const fontSizeMap = typography?.fontSize;
|
|
1805
|
-
const fontWeightMap = typography?.fontWeight;
|
|
1806
|
-
const lineHeightMap = typography?.lineHeight;
|
|
1807
|
-
const letterSpacingMap = typography?.letterSpacing;
|
|
1808
|
-
const textTransformMap = typography?.textTransform;
|
|
1809
|
-
const textDecorationMap = typography?.textDecoration;
|
|
1810
|
-
if (fontSizeMap && typeof fontSizeMap === "object" && Object.keys(fontSizeMap).length > 0) {
|
|
1811
|
-
for (const [key, sizeVal] of Object.entries(fontSizeMap)) {
|
|
1812
|
-
const fontSize = sizeToPx(sizeVal);
|
|
1813
|
-
if (fontSize <= 0) continue;
|
|
1814
|
-
const role = typesetKeyToFontFamilyRole(key);
|
|
1815
|
-
const fontFamily = toFontFamilyString(
|
|
1816
|
-
fontFamilyMap[role] ?? fontFamilyMap.body ?? fontFamilyMap.heading ?? fontFamilyMap.display ?? defaultFontFamily
|
|
1817
|
-
);
|
|
1818
|
-
const lh = lineHeightMap && typeof lineHeightMap === "object" ? lineHeightMap[key] : void 0;
|
|
1819
|
-
const weight = fontWeightMap && typeof fontWeightMap === "object" ? fontWeightMap[key] : void 0;
|
|
1820
|
-
const fontWeight = weight != null ? String(weight) : "400";
|
|
1821
|
-
const letterSpacingPx = letterSpacingToPx(
|
|
1822
|
-
letterSpacingMap && typeof letterSpacingMap === "object" ? letterSpacingMap[key] : void 0,
|
|
1823
|
-
fontSize
|
|
1824
|
-
);
|
|
1825
|
-
const textTransform = textTransformMap && typeof textTransformMap === "object" ? textTransformMap[key] : void 0;
|
|
1826
|
-
const textDecoration = textDecorationMap && typeof textDecorationMap === "object" ? textDecorationMap[key] : void 0;
|
|
1827
|
-
const namePart = key.replace(/-/g, " / ");
|
|
1828
|
-
const style = {
|
|
1829
|
-
name: namePart.startsWith("Typography") ? namePart : `Typography / ${namePart}`,
|
|
1830
|
-
fontFamily,
|
|
1831
|
-
fontWeight,
|
|
1832
|
-
fontSize,
|
|
1833
|
-
lineHeightUnit: "PERCENT",
|
|
1834
|
-
letterSpacingUnit: "PIXELS",
|
|
1835
|
-
...letterSpacingPx !== void 0 && letterSpacingPx !== 0 ? { letterSpacingValue: letterSpacingPx } : {}
|
|
1836
|
-
};
|
|
1837
|
-
if (lh != null && typeof lh === "number" && lh > 0) {
|
|
1838
|
-
style.lineHeightValue = lh >= 10 ? Math.round(lh / fontSize * 100) : Math.round(lh * 100);
|
|
1839
|
-
} else {
|
|
1840
|
-
style.lineHeightValue = 150;
|
|
1841
|
-
}
|
|
1842
|
-
if (textTransform === "uppercase") style.textCase = "UPPER";
|
|
1843
|
-
else if (textTransform === "lowercase") style.textCase = "LOWER";
|
|
1844
|
-
else if (textTransform === "capitalize") style.textCase = "TITLE";
|
|
1845
|
-
else style.textCase = "ORIGINAL";
|
|
1846
|
-
if (textDecoration === "underline") style.textDecoration = "UNDERLINE";
|
|
1847
|
-
else style.textDecoration = "NONE";
|
|
1848
|
-
textStyles.push(style);
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
const textStylesMap = typography?.textStyles;
|
|
1852
|
-
if (textStyles.length === 0 && textStylesMap && typeof textStylesMap === "object") {
|
|
1853
|
-
for (const [styleName, style] of Object.entries(textStylesMap)) {
|
|
1854
|
-
if (!style || typeof style !== "object") continue;
|
|
1855
|
-
const fontSize = sizeToPx(style.fontSize ?? "1rem");
|
|
1856
|
-
const lhStr = style.lineHeight;
|
|
1857
|
-
const lineHeightUnitless = lhStr != null ? lhStr.endsWith("%") ? parseFloat(lhStr) / 100 : sizeToPx(lhStr) / fontSize : 1.5;
|
|
1858
|
-
const payload = {
|
|
1859
|
-
name: styleName.startsWith("Typography") ? styleName : `Typography / ${styleName.replace(/\//g, " / ")}`,
|
|
1860
|
-
fontFamily: defaultFontFamily,
|
|
1861
|
-
fontWeight: String(style.fontWeight ?? "400"),
|
|
1862
|
-
fontSize,
|
|
1863
|
-
lineHeightUnit: "PERCENT",
|
|
1864
|
-
lineHeightValue: Math.round((Number.isFinite(lineHeightUnitless) ? lineHeightUnitless : 1.5) * 100),
|
|
1865
|
-
letterSpacingUnit: "PIXELS",
|
|
1866
|
-
textCase: "ORIGINAL",
|
|
1867
|
-
textDecoration: "NONE"
|
|
1868
|
-
};
|
|
1869
|
-
textStyles.push(payload);
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
textStyles.sort((a, b) => {
|
|
1873
|
-
if (a.fontSize !== b.fontSize) return a.fontSize - b.fontSize;
|
|
1874
|
-
return (a.name || "").localeCompare(b.name || "");
|
|
1875
|
-
});
|
|
1876
|
-
const numberVariableCollections = [];
|
|
1877
|
-
const spacing = tokens?.spacing;
|
|
1878
|
-
if (spacing?.scale && typeof spacing.scale === "object") {
|
|
1879
|
-
const vars = [];
|
|
1880
|
-
for (const [key, val] of Object.entries(spacing.scale)) {
|
|
1881
|
-
const n = tokenValueToNumber(val);
|
|
1882
|
-
if (n >= 0) vars.push({ name: `Spacing / ${key}`, value: n });
|
|
1883
|
-
}
|
|
1884
|
-
vars.sort((a, b) => a.value - b.value);
|
|
1885
|
-
if (vars.length > 0)
|
|
1886
|
-
numberVariableCollections.push({
|
|
1887
|
-
collectionName: `${collectionPrefix}Spacing`,
|
|
1888
|
-
categoryKey: "Spacing",
|
|
1889
|
-
variables: vars,
|
|
1890
|
-
scopes: ["GAP"]
|
|
1891
|
-
});
|
|
1892
|
-
}
|
|
1893
|
-
const radius = tokens?.radius;
|
|
1894
|
-
if (radius?.scale && typeof radius.scale === "object") {
|
|
1895
|
-
const vars = [];
|
|
1896
|
-
for (const [key, val] of Object.entries(radius.scale)) {
|
|
1897
|
-
const n = tokenValueToNumber(val);
|
|
1898
|
-
if (n >= 0) vars.push({ name: `Radius / ${key}`, value: n });
|
|
1899
|
-
}
|
|
1900
|
-
vars.sort((a, b) => a.value - b.value);
|
|
1901
|
-
if (vars.length > 0)
|
|
1902
|
-
numberVariableCollections.push({
|
|
1903
|
-
collectionName: `${collectionPrefix}Radius`,
|
|
1904
|
-
categoryKey: "Radius",
|
|
1905
|
-
variables: vars,
|
|
1906
|
-
scopes: ["CORNER_RADIUS"]
|
|
1907
|
-
});
|
|
1908
|
-
}
|
|
1909
|
-
const borders = tokens?.borders;
|
|
1910
|
-
if (borders?.width && typeof borders.width === "object") {
|
|
1911
|
-
const vars = [];
|
|
1912
|
-
for (const [key, val] of Object.entries(borders.width)) {
|
|
1913
|
-
const n = tokenValueToNumber(val);
|
|
1914
|
-
if (n >= 0) vars.push({ name: `Borders / ${key}`, value: n });
|
|
1915
|
-
}
|
|
1916
|
-
vars.sort((a, b) => a.value - b.value);
|
|
1917
|
-
if (vars.length > 0)
|
|
1918
|
-
numberVariableCollections.push({
|
|
1919
|
-
collectionName: `${collectionPrefix}Borders`,
|
|
1920
|
-
categoryKey: "Borders",
|
|
1921
|
-
variables: vars,
|
|
1922
|
-
scopes: ["STROKE_FLOAT"]
|
|
1923
|
-
});
|
|
1924
|
-
}
|
|
1925
|
-
const sizing = tokens?.sizing;
|
|
1926
|
-
const sizingVariables = [];
|
|
1927
|
-
if (sizing?.height && typeof sizing.height === "object") {
|
|
1928
|
-
for (const [key, val] of Object.entries(sizing.height)) {
|
|
1929
|
-
const n = tokenValueToNumber(val);
|
|
1930
|
-
if (n >= 0) sizingVariables.push({ name: `Height / ${key}`, value: n });
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
if (sizing?.icon && typeof sizing.icon === "object") {
|
|
1934
|
-
for (const [key, val] of Object.entries(sizing.icon)) {
|
|
1935
|
-
const n = tokenValueToNumber(val);
|
|
1936
|
-
if (n >= 0) sizingVariables.push({ name: `Icon / ${key}`, value: n });
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
sizingVariables.sort((a, b) => a.value - b.value);
|
|
1940
|
-
if (sizingVariables.length > 0) {
|
|
1941
|
-
numberVariableCollections.push({
|
|
1942
|
-
collectionName: `${collectionPrefix}Sizing`,
|
|
1943
|
-
categoryKey: "Sizing",
|
|
1944
|
-
variables: sizingVariables,
|
|
1945
|
-
scopes: ["WIDTH_HEIGHT"]
|
|
1946
|
-
});
|
|
1947
|
-
}
|
|
1948
|
-
const layout = tokens?.layout;
|
|
1949
|
-
if (layout?.breakpoint && typeof layout.breakpoint === "object") {
|
|
1950
|
-
const vars = [];
|
|
1951
|
-
for (const [key, val] of Object.entries(layout.breakpoint)) {
|
|
1952
|
-
const n = tokenValueToNumber(val);
|
|
1953
|
-
if (n >= 0) vars.push({ name: `Breakpoint / ${key}`, value: n });
|
|
1954
|
-
}
|
|
1955
|
-
vars.sort((a, b) => a.value - b.value);
|
|
1956
|
-
if (vars.length > 0)
|
|
1957
|
-
numberVariableCollections.push({
|
|
1958
|
-
collectionName: `${collectionPrefix}Layout`,
|
|
1959
|
-
categoryKey: "Layout",
|
|
1960
|
-
variables: vars,
|
|
1961
|
-
scopes: ["WIDTH_HEIGHT"]
|
|
1962
|
-
});
|
|
1963
|
-
}
|
|
1964
|
-
const effectStyles = [];
|
|
1965
|
-
const shadows = tokens?.shadows;
|
|
1966
|
-
if (shadows?.elevation && typeof shadows.elevation === "object") {
|
|
1967
|
-
for (const [key, val] of Object.entries(shadows.elevation)) {
|
|
1968
|
-
if (typeof val !== "string") continue;
|
|
1969
|
-
const effects = parseBoxShadowToFigmaEffects(val);
|
|
1970
|
-
if (effects.length > 0) effectStyles.push({ name: `Shadow / ${key}`, effects });
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
if (shadows?.focus && typeof shadows.focus === "string") {
|
|
1974
|
-
const effects = parseBoxShadowToFigmaEffects(shadows.focus);
|
|
1975
|
-
if (effects.length > 0) effectStyles.push({ name: "Shadow / focus", effects });
|
|
1976
|
-
}
|
|
1977
|
-
effectStyles.sort((a, b) => {
|
|
1978
|
-
const nameA = a.name.startsWith("Shadow / ") ? a.name.slice(9) : a.name;
|
|
1979
|
-
const nameB = b.name.startsWith("Shadow / ") ? b.name.slice(9) : b.name;
|
|
1980
|
-
const orderA = FIGMA_SHADOW_ORDER[nameA] ?? 100;
|
|
1981
|
-
const orderB = FIGMA_SHADOW_ORDER[nameB] ?? 100;
|
|
1982
|
-
if (orderA !== orderB) return orderA - orderB;
|
|
1983
|
-
return nameA.localeCompare(nameB);
|
|
1984
|
-
});
|
|
1985
|
-
return {
|
|
1986
|
-
colorVariableCollections,
|
|
1987
|
-
paintStyles,
|
|
1988
|
-
textStyles,
|
|
1989
|
-
numberVariableCollections,
|
|
1990
|
-
effectStyles
|
|
1991
|
-
};
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
1227
|
// src/index.ts
|
|
1995
|
-
import * as
|
|
1996
|
-
import * as
|
|
1228
|
+
import * as path from "path";
|
|
1229
|
+
import * as fs from "fs";
|
|
1997
1230
|
import { execSync } from "child_process";
|
|
1998
1231
|
import { platform } from "os";
|
|
1999
1232
|
import WebSocket, { WebSocketServer } from "ws";
|
|
@@ -2152,13 +1385,13 @@ function sendBridgeRequest(method, params, timeoutMs = FIGMA_BRIDGE_TIMEOUT_MS)
|
|
|
2152
1385
|
);
|
|
2153
1386
|
}
|
|
2154
1387
|
const id = `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
2155
|
-
return new Promise((
|
|
1388
|
+
return new Promise((resolve3, reject) => {
|
|
2156
1389
|
const timeout = setTimeout(() => {
|
|
2157
1390
|
if (pendingBridgeRequests.delete(id)) {
|
|
2158
1391
|
reject(new Error("Figma bridge timeout. " + FIGMA_CONNECTION_INSTRUCTIONS.startBridge + " Then " + FIGMA_CONNECTION_INSTRUCTIONS.connect));
|
|
2159
1392
|
}
|
|
2160
1393
|
}, timeoutMs);
|
|
2161
|
-
pendingBridgeRequests.set(id, { resolve:
|
|
1394
|
+
pendingBridgeRequests.set(id, { resolve: resolve3, reject, timeout });
|
|
2162
1395
|
try {
|
|
2163
1396
|
ws.send(JSON.stringify({ id, method: normalized, params }));
|
|
2164
1397
|
} catch (e) {
|
|
@@ -2194,10 +1427,25 @@ function parseArgs() {
|
|
|
2194
1427
|
var cliArgs = parseArgs();
|
|
2195
1428
|
var { dsId, apiKey, accessToken } = cliArgs;
|
|
2196
1429
|
var apiBase = cliArgs.apiBase || "https://atomix.studio";
|
|
1430
|
+
var MCP_VERSION = "1.0.34";
|
|
2197
1431
|
var cachedData = null;
|
|
2198
1432
|
var cachedETag = null;
|
|
2199
1433
|
var cachedMcpTier = null;
|
|
2200
1434
|
var authFailedNoTools = false;
|
|
1435
|
+
var mcpUpdateNotice = null;
|
|
1436
|
+
var mcpLatestVersion = null;
|
|
1437
|
+
function isVersionNewer(latest, current) {
|
|
1438
|
+
const toParts = (v) => v.split(".").map((n) => parseInt(n, 10) || 0);
|
|
1439
|
+
const a = toParts(latest);
|
|
1440
|
+
const b = toParts(current);
|
|
1441
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
1442
|
+
const x = a[i] ?? 0;
|
|
1443
|
+
const y = b[i] ?? 0;
|
|
1444
|
+
if (x > y) return true;
|
|
1445
|
+
if (x < y) return false;
|
|
1446
|
+
}
|
|
1447
|
+
return false;
|
|
1448
|
+
}
|
|
2201
1449
|
function hasValidAuthConfig() {
|
|
2202
1450
|
return !!(dsId && accessToken);
|
|
2203
1451
|
}
|
|
@@ -2218,10 +1466,10 @@ ${changes.summary}`);
|
|
|
2218
1466
|
}
|
|
2219
1467
|
}
|
|
2220
1468
|
function validateTokenFileAfterWrite(outputPath, format, expectedMinVariables) {
|
|
2221
|
-
if (!
|
|
1469
|
+
if (!fs.existsSync(outputPath)) {
|
|
2222
1470
|
return { path: outputPath, status: "FAIL", detail: "File not found after write." };
|
|
2223
1471
|
}
|
|
2224
|
-
const content =
|
|
1472
|
+
const content = fs.readFileSync(outputPath, "utf-8");
|
|
2225
1473
|
if (!content || content.trim().length === 0) {
|
|
2226
1474
|
return { path: outputPath, status: "FAIL", detail: "File is empty after write." };
|
|
2227
1475
|
}
|
|
@@ -2243,7 +1491,7 @@ function validateTokenFileAfterWrite(outputPath, format, expectedMinVariables) {
|
|
|
2243
1491
|
}
|
|
2244
1492
|
function formatValidationBlock(entries) {
|
|
2245
1493
|
if (entries.length === 0) return "";
|
|
2246
|
-
const displayPath = (p) => p.startsWith("(") ? p :
|
|
1494
|
+
const displayPath = (p) => p.startsWith("(") ? p : path.relative(process.cwd(), p);
|
|
2247
1495
|
const lines = [
|
|
2248
1496
|
"",
|
|
2249
1497
|
"\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
|
|
@@ -2273,11 +1521,19 @@ async function fetchDesignSystemForMCP(forceRefresh = false) {
|
|
|
2273
1521
|
cachedData = result.data;
|
|
2274
1522
|
cachedETag = result.etag;
|
|
2275
1523
|
cachedMcpTier = result.data.meta.mcpTier ?? null;
|
|
1524
|
+
const latest = result.data.meta.mcpLatestVersion;
|
|
1525
|
+
if (latest && isVersionNewer(latest, MCP_VERSION)) {
|
|
1526
|
+
mcpLatestVersion = latest;
|
|
1527
|
+
mcpUpdateNotice = `**MCP update available:** A new Atomix MCP server (v${latest}) is available. You're on v${MCP_VERSION}. To use the new version in Cursor: 1) Quit Cursor completely and reopen, 2) Clear npx cache: \`rm -rf ~/.npm/_npx\` (macOS/Linux), 3) Ensure your MCP config uses \`@atomixstudio/mcp@latest\` or \`@atomixstudio/mcp@${latest}\`. To test a local build before publishing: point MCP to \`node /path/to/Atom/packages/mcp-user/dist/index.js\` with \`--ds-id\` and \`--atomix-token\`.`;
|
|
1528
|
+
} else {
|
|
1529
|
+
mcpUpdateNotice = null;
|
|
1530
|
+
mcpLatestVersion = null;
|
|
1531
|
+
}
|
|
2276
1532
|
await updateChangeSummary(result.data);
|
|
2277
1533
|
return result.data;
|
|
2278
1534
|
}
|
|
2279
1535
|
var TOKEN_CATEGORIES = ["colors", "typography", "spacing", "sizing", "shadows", "radius", "borders", "motion", "zIndex"];
|
|
2280
|
-
function
|
|
1536
|
+
function typesetKeyToFontFamilyRole(key) {
|
|
2281
1537
|
const prefix = (key.split("-")[0] ?? key).toLowerCase();
|
|
2282
1538
|
if (prefix === "display" || prefix.startsWith("display")) return "display";
|
|
2283
1539
|
if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
|
|
@@ -2297,7 +1553,7 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
|
|
|
2297
1553
|
const p = cssPrefix ? `${cssPrefix}-` : "";
|
|
2298
1554
|
const typesets = [];
|
|
2299
1555
|
for (const key of Object.keys(fontSize)) {
|
|
2300
|
-
const role =
|
|
1556
|
+
const role = typesetKeyToFontFamilyRole(key);
|
|
2301
1557
|
const familyName = fontFamily[role] ?? fontFamily.body;
|
|
2302
1558
|
const fontFamilyVarName = familyName ? `--${p}typography-font-family-${role}` : void 0;
|
|
2303
1559
|
const fontFamilyVar = familyName ? `var(${fontFamilyVarName})` : "";
|
|
@@ -2323,7 +1579,7 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
|
|
|
2323
1579
|
var server = new Server(
|
|
2324
1580
|
{
|
|
2325
1581
|
name: "atomix-mcp-user",
|
|
2326
|
-
version:
|
|
1582
|
+
version: MCP_VERSION
|
|
2327
1583
|
},
|
|
2328
1584
|
{
|
|
2329
1585
|
capabilities: {
|
|
@@ -2441,18 +1697,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2441
1697
|
}
|
|
2442
1698
|
},
|
|
2443
1699
|
{
|
|
2444
|
-
name: "
|
|
2445
|
-
description: "
|
|
1700
|
+
name: "getRules",
|
|
1701
|
+
description: "Get design system governance rules. Optionally filter by topic (colors, typo, motion, icons, layout, visual). Call at session start or before writing visual code.",
|
|
2446
1702
|
inputSchema: {
|
|
2447
1703
|
type: "object",
|
|
2448
1704
|
properties: {
|
|
2449
|
-
|
|
1705
|
+
topic: {
|
|
2450
1706
|
type: "string",
|
|
2451
|
-
enum: ["
|
|
2452
|
-
description: "
|
|
1707
|
+
enum: ["colors", "typo", "typography", "motion", "icons", "layout", "visual", "style"],
|
|
1708
|
+
description: "Optional. Filter rules by topic: colors, typo/typography, motion, icons, layout, or visual/style (color, border, radius, shadows, icons). Omit for all rules."
|
|
2453
1709
|
}
|
|
2454
|
-
}
|
|
2455
|
-
required: ["tool"]
|
|
1710
|
+
}
|
|
2456
1711
|
}
|
|
2457
1712
|
},
|
|
2458
1713
|
{
|
|
@@ -2487,10 +1742,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2487
1742
|
},
|
|
2488
1743
|
{
|
|
2489
1744
|
name: "syncAll",
|
|
2490
|
-
description: "Sync tokens, AI rules, skills (.cursor/skills/atomix-ds/SKILL.md), and atomix-dependencies.json. Use dryRun: true first to report what would change without writing; then dryRun: false to apply.
|
|
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.",
|
|
2491
1746
|
inputSchema: {
|
|
2492
1747
|
type: "object",
|
|
2493
1748
|
properties: {
|
|
1749
|
+
workspaceRoot: {
|
|
1750
|
+
type: "string",
|
|
1751
|
+
description: "Absolute path to the project/repo root. Skills and manifest are written under this path so they can be committed. If omitted, uses ATOMIX_PROJECT_ROOT env var, then process.cwd()."
|
|
1752
|
+
},
|
|
2494
1753
|
output: {
|
|
2495
1754
|
type: "string",
|
|
2496
1755
|
description: "Token file path (e.g. ./tokens.css). Default: ./tokens.css. Ignored if skipTokens is true."
|
|
@@ -2531,6 +1790,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2531
1790
|
required: []
|
|
2532
1791
|
}
|
|
2533
1792
|
},
|
|
1793
|
+
{
|
|
1794
|
+
name: "getMcpVersion",
|
|
1795
|
+
description: "Return the current Atomix MCP server version (e.g. 1.0.33) and, if known, the latest available from the API. Call this whenever the user asks about MCP version, Atomix MCP version, what version of the MCP server they are using, or whether an update is available. Prefer this over explaining the MCP protocol spec version.",
|
|
1796
|
+
inputSchema: {
|
|
1797
|
+
type: "object",
|
|
1798
|
+
properties: {},
|
|
1799
|
+
required: []
|
|
1800
|
+
}
|
|
1801
|
+
},
|
|
2534
1802
|
{
|
|
2535
1803
|
name: "syncToFigma",
|
|
2536
1804
|
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'.",
|
|
@@ -2541,6 +1809,43 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2541
1809
|
}
|
|
2542
1810
|
}
|
|
2543
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
|
+
}
|
|
2544
1849
|
return { tools: toolsList };
|
|
2545
1850
|
});
|
|
2546
1851
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -2554,18 +1859,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2554
1859
|
isError: true
|
|
2555
1860
|
};
|
|
2556
1861
|
}
|
|
1862
|
+
if (name === "getMcpVersion") {
|
|
1863
|
+
const out = {
|
|
1864
|
+
version: MCP_VERSION,
|
|
1865
|
+
name: "atomix-mcp-user"
|
|
1866
|
+
};
|
|
1867
|
+
if (mcpLatestVersion) {
|
|
1868
|
+
out.latestVersion = mcpLatestVersion;
|
|
1869
|
+
out.updateAvailable = true;
|
|
1870
|
+
}
|
|
1871
|
+
return {
|
|
1872
|
+
content: [{ type: "text", text: JSON.stringify(out, null, 2) }]
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
2557
1875
|
try {
|
|
2558
1876
|
const shouldForceRefresh = name === "syncAll";
|
|
2559
1877
|
const data = await fetchDesignSystemForMCP(shouldForceRefresh);
|
|
2560
|
-
async function performTokenSyncAndRules(designSystemData, tokenOutput, tokenFormat, dryRun) {
|
|
1878
|
+
async function performTokenSyncAndRules(designSystemData, tokenOutput, tokenFormat, dryRun, projectRoot) {
|
|
2561
1879
|
const output = tokenOutput;
|
|
2562
1880
|
const format = tokenFormat;
|
|
2563
|
-
const outputPath =
|
|
2564
|
-
const fileExists =
|
|
1881
|
+
const outputPath = path.resolve(projectRoot, output);
|
|
1882
|
+
const fileExists = fs.existsSync(outputPath);
|
|
2565
1883
|
const deprecatedTokens = /* @__PURE__ */ new Map();
|
|
2566
1884
|
const existingTokens = /* @__PURE__ */ new Map();
|
|
2567
1885
|
if (fileExists && ["css", "scss", "less"].includes(format)) {
|
|
2568
|
-
const oldContent =
|
|
1886
|
+
const oldContent = fs.readFileSync(outputPath, "utf-8");
|
|
2569
1887
|
const oldVarPattern = /(?:^|\n)\s*(?:\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\/\s*)?(--[a-zA-Z0-9-]+):\s*([^;]+);/gm;
|
|
2570
1888
|
let match;
|
|
2571
1889
|
while ((match = oldVarPattern.exec(oldContent)) !== null) {
|
|
@@ -2615,7 +1933,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2615
1933
|
let changes = [];
|
|
2616
1934
|
let diff;
|
|
2617
1935
|
if (fileExists && ["css", "scss", "less"].includes(format)) {
|
|
2618
|
-
const oldContent =
|
|
1936
|
+
const oldContent = fs.readFileSync(outputPath, "utf-8");
|
|
2619
1937
|
diff = diffTokens(oldContent, mergedCssVariables, format, darkModeColors?.dark);
|
|
2620
1938
|
const lightChanges = diff.added.length + diff.modified.length;
|
|
2621
1939
|
const darkChanges = diff.addedDark.length + diff.modifiedDark.length;
|
|
@@ -2670,7 +1988,7 @@ Version: ${designSystemData.meta.version}`,
|
|
|
2670
1988
|
` Tokens: ${tokenCount} (${deprecatedCount} deprecated preserved)`,
|
|
2671
1989
|
changeLine,
|
|
2672
1990
|
"",
|
|
2673
|
-
"
|
|
1991
|
+
"Would write skills: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/figma-design-SKILL.md",
|
|
2674
1992
|
"",
|
|
2675
1993
|
"Call syncAll again with dryRun: false to apply."
|
|
2676
1994
|
].filter(Boolean).join("\n");
|
|
@@ -2680,30 +1998,12 @@ Version: ${designSystemData.meta.version}`,
|
|
|
2680
1998
|
validation: [{ path: "(dry run)", status: "OK", detail: "No files written." }]
|
|
2681
1999
|
};
|
|
2682
2000
|
}
|
|
2683
|
-
const outputDir =
|
|
2684
|
-
if (!
|
|
2685
|
-
|
|
2001
|
+
const outputDir = path.dirname(outputPath);
|
|
2002
|
+
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
2003
|
+
fs.writeFileSync(outputPath, newContent);
|
|
2686
2004
|
const validation = [];
|
|
2687
2005
|
validation.push(validateTokenFileAfterWrite(outputPath, format, tokenCount));
|
|
2688
|
-
|
|
2689
|
-
try {
|
|
2690
|
-
rulesResults = await syncRulesFiles({
|
|
2691
|
-
dsId,
|
|
2692
|
-
apiKey: apiKey ?? void 0,
|
|
2693
|
-
apiBase: apiBase ?? void 0,
|
|
2694
|
-
rulesDir: process.cwd()
|
|
2695
|
-
});
|
|
2696
|
-
for (const r of rulesResults) {
|
|
2697
|
-
const fullPath = path2.resolve(process.cwd(), r.path);
|
|
2698
|
-
validation.push({
|
|
2699
|
-
path: fullPath,
|
|
2700
|
-
status: r.success && fs2.existsSync(fullPath) ? "OK" : "FAIL",
|
|
2701
|
-
detail: r.success ? "Written." : r.error || "Write failed."
|
|
2702
|
-
});
|
|
2703
|
-
}
|
|
2704
|
-
} catch (error) {
|
|
2705
|
-
console.error(`[syncAll] Failed to sync rules: ${error}`);
|
|
2706
|
-
}
|
|
2006
|
+
const rulesResults = [];
|
|
2707
2007
|
const governanceChanges = cachedData ? detectGovernanceChangesByFoundation(cachedData, designSystemData) : [];
|
|
2708
2008
|
const response = formatSyncResponse({
|
|
2709
2009
|
data: designSystemData,
|
|
@@ -2721,31 +2021,32 @@ Version: ${designSystemData.meta.version}`,
|
|
|
2721
2021
|
hasRefactorRecommendation: !!lastSyncAffectedTokens?.removed.length,
|
|
2722
2022
|
deprecatedTokenCount: lastSyncAffectedTokens?.removed.length || 0
|
|
2723
2023
|
});
|
|
2724
|
-
|
|
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 };
|
|
2725
2026
|
}
|
|
2726
2027
|
switch (name) {
|
|
2727
2028
|
case "getToken": {
|
|
2728
|
-
const
|
|
2729
|
-
const value = getTokenByPath(data.tokens,
|
|
2029
|
+
const path2 = args?.path;
|
|
2030
|
+
const value = getTokenByPath(data.tokens, path2);
|
|
2730
2031
|
if (value === void 0) {
|
|
2731
2032
|
return {
|
|
2732
2033
|
content: [{
|
|
2733
2034
|
type: "text",
|
|
2734
2035
|
text: JSON.stringify({
|
|
2735
|
-
error: `Token not found: ${
|
|
2036
|
+
error: `Token not found: ${path2}`,
|
|
2736
2037
|
suggestion: "Use listTokens or searchTokens to find available tokens.",
|
|
2737
2038
|
availableCategories: TOKEN_CATEGORIES
|
|
2738
2039
|
}, null, 2)
|
|
2739
2040
|
}]
|
|
2740
2041
|
};
|
|
2741
2042
|
}
|
|
2742
|
-
const cssVarKey = `--atmx-${
|
|
2043
|
+
const cssVarKey = `--atmx-${path2.replace(/\./g, "-")}`;
|
|
2743
2044
|
const cssVar = data.cssVariables[cssVarKey];
|
|
2744
2045
|
return {
|
|
2745
2046
|
content: [{
|
|
2746
2047
|
type: "text",
|
|
2747
2048
|
text: JSON.stringify({
|
|
2748
|
-
path:
|
|
2049
|
+
path: path2,
|
|
2749
2050
|
value,
|
|
2750
2051
|
cssVariable: cssVar || `var(${cssVarKey})`,
|
|
2751
2052
|
usage: `style={{ property: "var(${cssVarKey})" }}`
|
|
@@ -2772,13 +2073,13 @@ Version: ${designSystemData.meta.version}`,
|
|
|
2772
2073
|
};
|
|
2773
2074
|
}
|
|
2774
2075
|
const flat = flattenTokens(tokensToList);
|
|
2775
|
-
const tokensWithCssVars = flat.map(({ path:
|
|
2776
|
-
const fullPath = subcategory ? `${category}.${subcategory}.${
|
|
2076
|
+
const tokensWithCssVars = flat.map(({ path: path2, value }) => {
|
|
2077
|
+
const fullPath = subcategory ? `${category}.${subcategory}.${path2}` : `${category}.${path2}`;
|
|
2777
2078
|
let cssVar;
|
|
2778
2079
|
if (category === "colors" && subcategory === "static.brand") {
|
|
2779
|
-
cssVar = data.cssVariables[`--atmx-color-brand-${
|
|
2080
|
+
cssVar = data.cssVariables[`--atmx-color-brand-${path2}`];
|
|
2780
2081
|
} else if (category === "colors" && subcategory?.startsWith("modes.")) {
|
|
2781
|
-
cssVar = data.cssVariables[`--atmx-color-${
|
|
2082
|
+
cssVar = data.cssVariables[`--atmx-color-${path2}`];
|
|
2782
2083
|
} else {
|
|
2783
2084
|
const cssVarKey = `--atmx-${fullPath.replace(/\./g, "-")}`;
|
|
2784
2085
|
cssVar = data.cssVariables[cssVarKey];
|
|
@@ -2887,30 +2188,63 @@ Version: ${designSystemData.meta.version}`,
|
|
|
2887
2188
|
}]
|
|
2888
2189
|
};
|
|
2889
2190
|
}
|
|
2890
|
-
case "
|
|
2891
|
-
const
|
|
2892
|
-
const
|
|
2893
|
-
|
|
2191
|
+
case "getRules": {
|
|
2192
|
+
const topicRaw = args?.topic;
|
|
2193
|
+
const topic = topicRaw?.toLowerCase().trim();
|
|
2194
|
+
const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
|
|
2195
|
+
console.error(`[getRules] Fetching: ${rulesUrl}${topic ? ` topic=${topic}` : ""}`);
|
|
2894
2196
|
const headers = { "Content-Type": "application/json" };
|
|
2895
2197
|
if (apiKey) headers["x-api-key"] = apiKey;
|
|
2896
2198
|
try {
|
|
2897
2199
|
const response = await fetch(rulesUrl, { headers });
|
|
2898
|
-
console.error(`[
|
|
2200
|
+
console.error(`[getRules] Response status: ${response.status}`);
|
|
2899
2201
|
if (!response.ok) {
|
|
2900
2202
|
const errorText = await response.text();
|
|
2901
|
-
console.error(`[
|
|
2203
|
+
console.error(`[getRules] Error response: ${errorText}`);
|
|
2902
2204
|
throw new Error(`Failed to fetch rules: ${response.status} - ${errorText}`);
|
|
2903
2205
|
}
|
|
2904
|
-
const
|
|
2905
|
-
|
|
2206
|
+
const payload = await response.json();
|
|
2207
|
+
const categories = payload.categories ?? {};
|
|
2208
|
+
const allRules = payload.rules ?? [];
|
|
2209
|
+
if (!topic) {
|
|
2210
|
+
return {
|
|
2211
|
+
content: [{ type: "text", text: JSON.stringify({ rules: allRules, categories }, null, 2) }]
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
const topicToCategories = {
|
|
2215
|
+
colors: ["general", "colors"],
|
|
2216
|
+
typo: ["general", "typography"],
|
|
2217
|
+
typography: ["general", "typography"],
|
|
2218
|
+
motion: ["general", "motion"],
|
|
2219
|
+
icons: ["general", "icons"],
|
|
2220
|
+
layout: ["general", "spacing", "sizing", "layout"],
|
|
2221
|
+
visual: ["general", "colors", "borders", "radius", "shadows", "icons"],
|
|
2222
|
+
style: ["general", "colors", "borders", "radius", "shadows", "icons"]
|
|
2223
|
+
};
|
|
2224
|
+
const categoryKeys = topicToCategories[topic];
|
|
2225
|
+
if (!categoryKeys) {
|
|
2226
|
+
return {
|
|
2227
|
+
content: [{ type: "text", text: JSON.stringify({ rules: allRules, categories }, null, 2) }]
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
const filteredCategories = {};
|
|
2231
|
+
const filteredRules = [];
|
|
2232
|
+
for (const key of categoryKeys) {
|
|
2233
|
+
const list = categories[key];
|
|
2234
|
+
if (list && list.length > 0) {
|
|
2235
|
+
filteredCategories[key] = list;
|
|
2236
|
+
filteredRules.push(...list);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
console.error(`[getRules] Got ${filteredRules.length} rules for topic=${topic}`);
|
|
2906
2240
|
return {
|
|
2907
2241
|
content: [{
|
|
2908
2242
|
type: "text",
|
|
2909
|
-
text: JSON.stringify(rules, null, 2)
|
|
2243
|
+
text: JSON.stringify({ rules: filteredRules, categories: filteredCategories }, null, 2)
|
|
2910
2244
|
}]
|
|
2911
2245
|
};
|
|
2912
2246
|
} catch (fetchError) {
|
|
2913
|
-
console.error(`[
|
|
2247
|
+
console.error(`[getRules] Fetch error:`, fetchError);
|
|
2914
2248
|
throw fetchError;
|
|
2915
2249
|
}
|
|
2916
2250
|
}
|
|
@@ -2995,7 +2329,7 @@ Version: ${designSystemData.meta.version}`,
|
|
|
2995
2329
|
copilot: `# Copilot Setup
|
|
2996
2330
|
|
|
2997
2331
|
1. Create a Copilot instructions file in your project (e.g. \`.github/copilot-instructions.md\`)
|
|
2998
|
-
2.
|
|
2332
|
+
2. Run /--sync to write the skill; call getRules() when you need governance rules
|
|
2999
2333
|
3. Enable custom instructions in your editor (e.g. \`github.copilot.chat.codeGeneration.useInstructionFiles\`: true in settings)
|
|
3000
2334
|
|
|
3001
2335
|
## File Structure
|
|
@@ -3009,7 +2343,7 @@ Version: ${designSystemData.meta.version}`,
|
|
|
3009
2343
|
windsurf: `# Windsurf Setup
|
|
3010
2344
|
|
|
3011
2345
|
1. Create \`.windsurf/mcp.json\` in your project root
|
|
3012
|
-
2.
|
|
2346
|
+
2. Run /--sync to write the skill; call getRules() when you need governance rules
|
|
3013
2347
|
3. Restart Windsurf Editor
|
|
3014
2348
|
|
|
3015
2349
|
## File Structure
|
|
@@ -3050,7 +2384,7 @@ Version: ${designSystemData.meta.version}`,
|
|
|
3050
2384
|
zed: `# Zed Setup
|
|
3051
2385
|
|
|
3052
2386
|
1. Create \`.zed/assistant/rules.md\` in your project
|
|
3053
|
-
2.
|
|
2387
|
+
2. Run /--sync to write the skill; call getRules() for governance rules
|
|
3054
2388
|
|
|
3055
2389
|
## File Structure
|
|
3056
2390
|
|
|
@@ -3078,9 +2412,8 @@ Version: ${designSystemData.meta.version}`,
|
|
|
3078
2412
|
**Best Practice**: Keep \`tokens.css\` separate from your custom CSS. Use a separate file (e.g., \`custom.css\`) for custom styles.`,
|
|
3079
2413
|
generic: `# Generic AI Tool Setup
|
|
3080
2414
|
|
|
3081
|
-
1.
|
|
3082
|
-
2.
|
|
3083
|
-
3. Reference in your prompts or context
|
|
2415
|
+
1. Run /--sync to write the skill (.cursor/skills/atomix-ds/SKILL.md)
|
|
2416
|
+
2. Call getRules() when you need governance rules; reference in your prompts or context
|
|
3084
2417
|
|
|
3085
2418
|
## File Structure
|
|
3086
2419
|
|
|
@@ -3115,16 +2448,18 @@ Version: ${designSystemData.meta.version}`,
|
|
|
3115
2448
|
const dryRun = args?.dryRun === true;
|
|
3116
2449
|
const output = args?.output || "./tokens.css";
|
|
3117
2450
|
const format = args?.format || "css";
|
|
2451
|
+
const projectRoot = path.resolve(
|
|
2452
|
+
args?.workspaceRoot || process.env.ATOMIX_PROJECT_ROOT || process.cwd()
|
|
2453
|
+
);
|
|
3118
2454
|
const parts = [dryRun ? "[DRY RUN] syncAll report (no files written)." : "\u2713 syncAll complete."];
|
|
3119
2455
|
let tokenResponseText = "";
|
|
3120
2456
|
const allValidation = [];
|
|
2457
|
+
let tokenSyncChangeSummary;
|
|
3121
2458
|
if (!skipTokens) {
|
|
3122
|
-
const result = await performTokenSyncAndRules(data, output, format, dryRun);
|
|
2459
|
+
const result = await performTokenSyncAndRules(data, output, format, dryRun, projectRoot);
|
|
3123
2460
|
tokenResponseText = result.responseText;
|
|
3124
2461
|
allValidation.push(...result.validation);
|
|
3125
|
-
|
|
3126
|
-
parts.push(`Rules: ${result.rulesResults.map((r) => r.path).join(", ")}`);
|
|
3127
|
-
}
|
|
2462
|
+
tokenSyncChangeSummary = result.tokenSyncChangeSummary;
|
|
3128
2463
|
if (dryRun) {
|
|
3129
2464
|
parts.push(`Would write tokens: ${output} (${format})`);
|
|
3130
2465
|
} else {
|
|
@@ -3133,11 +2468,12 @@ Version: ${designSystemData.meta.version}`,
|
|
|
3133
2468
|
}
|
|
3134
2469
|
const dsVersion = String(data.meta.version ?? "1.0.0");
|
|
3135
2470
|
const dsExportedAt = data.meta.exportedAt;
|
|
3136
|
-
const skillsDir =
|
|
3137
|
-
const
|
|
3138
|
-
const
|
|
2471
|
+
const skillsDir = path.resolve(projectRoot, ".cursor/skills/atomix-ds");
|
|
2472
|
+
const manifestPath = path.resolve(projectRoot, "atomix-dependencies.json");
|
|
2473
|
+
const dependencySkills = getSyncDependencySkills(data, dsVersion, dsExportedAt);
|
|
3139
2474
|
if (dryRun) {
|
|
3140
|
-
|
|
2475
|
+
const skillList = dependencySkills.map((s) => s.path).join(", ");
|
|
2476
|
+
parts.push(`Would write skills: ${skillList}`);
|
|
3141
2477
|
parts.push("Would write manifest: atomix-dependencies.json");
|
|
3142
2478
|
const reportText = [parts.join("\n"), tokenResponseText].filter(Boolean).join("\n\n---\n\n");
|
|
3143
2479
|
return {
|
|
@@ -3146,11 +2482,19 @@ Version: ${designSystemData.meta.version}`,
|
|
|
3146
2482
|
${reportText}` }]
|
|
3147
2483
|
};
|
|
3148
2484
|
}
|
|
3149
|
-
if (!
|
|
3150
|
-
const
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
2485
|
+
if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
|
|
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})`);
|
|
3154
2498
|
const tokens = data.tokens;
|
|
3155
2499
|
const typography = tokens?.typography;
|
|
3156
2500
|
const fontFamily = typography?.fontFamily;
|
|
@@ -3169,6 +2513,10 @@ ${reportText}` }]
|
|
|
3169
2513
|
};
|
|
3170
2514
|
const lib = icons?.library || "lucide";
|
|
3171
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
|
+
}
|
|
3172
2520
|
const manifest = {
|
|
3173
2521
|
designSystem: { name: data.meta.name, version: data.meta.version },
|
|
3174
2522
|
tokenFile: skipTokens ? void 0 : output,
|
|
@@ -3179,23 +2527,67 @@ ${reportText}` }]
|
|
|
3179
2527
|
strokeWidthValue: icons?.strokeWidth
|
|
3180
2528
|
},
|
|
3181
2529
|
fonts: { families: fontNames },
|
|
3182
|
-
skills:
|
|
3183
|
-
skill: ".cursor/skills/atomix-ds/SKILL.md",
|
|
3184
|
-
syncedAtVersion: data.meta.version ?? "1.0.0"
|
|
3185
|
-
}
|
|
2530
|
+
skills: skillsManifestEntry
|
|
3186
2531
|
};
|
|
3187
|
-
|
|
3188
|
-
|
|
2532
|
+
const manifestExisted = fs.existsSync(manifestPath);
|
|
2533
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
2534
|
+
allValidation.push({ path: manifestPath, status: fs.existsSync(manifestPath) ? "OK" : "FAIL", detail: "Written." });
|
|
3189
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");
|
|
3190
2584
|
const summary = parts.join("\n");
|
|
3191
2585
|
const validationBlock = formatValidationBlock(allValidation);
|
|
3192
2586
|
const hasFailure = allValidation.some((e) => e.status === "FAIL");
|
|
3193
2587
|
const resultLine = hasFailure ? "syncAllResult: FAIL \u2014 Check VALIDATION section below. Do not report success to the user.\n\n" : "syncAllResult: OK\n\n";
|
|
3194
|
-
const fullText = resultLine + (tokenResponseText ?
|
|
3195
|
-
|
|
3196
|
-
---
|
|
2588
|
+
const fullText = resultLine + changesList + "\n\n" + (tokenResponseText ? `---
|
|
3197
2589
|
|
|
3198
|
-
${tokenResponseText}
|
|
2590
|
+
${tokenResponseText}` : "") + validationBlock;
|
|
3199
2591
|
return {
|
|
3200
2592
|
content: [{ type: "text", text: fullText }]
|
|
3201
2593
|
};
|
|
@@ -3233,10 +2625,17 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
|
|
|
3233
2625
|
families: fontNames,
|
|
3234
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."
|
|
3235
2627
|
},
|
|
3236
|
-
skill: {
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
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
|
+
} : {},
|
|
3240
2639
|
tokenFiles: {
|
|
3241
2640
|
files: ["tokens.css", "tokens.json"],
|
|
3242
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."
|
|
@@ -3244,7 +2643,7 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
|
|
|
3244
2643
|
showcase: platform2 === "web" || !platform2 ? {
|
|
3245
2644
|
path: "atomix-setup-showcase.html",
|
|
3246
2645
|
template: SHOWCASE_HTML_TEMPLATE,
|
|
3247
|
-
substitutionInstructions: '
|
|
2646
|
+
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.'
|
|
3248
2647
|
} : void 0,
|
|
3249
2648
|
meta: {
|
|
3250
2649
|
dsName: data.meta.name,
|
|
@@ -3461,13 +2860,282 @@ ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
|
|
|
3461
2860
|
...out.error ? { isError: true } : {}
|
|
3462
2861
|
};
|
|
3463
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
|
+
}
|
|
3464
3132
|
default:
|
|
3465
3133
|
return {
|
|
3466
3134
|
content: [{
|
|
3467
3135
|
type: "text",
|
|
3468
3136
|
text: JSON.stringify({
|
|
3469
3137
|
error: `Unknown tool: ${name}`,
|
|
3470
|
-
availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "
|
|
3138
|
+
availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "getMcpVersion", "syncToFigma", "designInFigma"]
|
|
3471
3139
|
}, null, 2)
|
|
3472
3140
|
}]
|
|
3473
3141
|
};
|
|
@@ -3509,6 +3177,43 @@ ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
|
|
|
3509
3177
|
};
|
|
3510
3178
|
}
|
|
3511
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
|
+
}
|
|
3512
3217
|
function injectSkillVersion(content, version, exportedAt) {
|
|
3513
3218
|
const endOfFrontmatter = content.indexOf("\n---\n", 3);
|
|
3514
3219
|
if (endOfFrontmatter === -1) return content;
|
|
@@ -3521,51 +3226,124 @@ atomixDsExportedAt: "${exportedAt ?? ""}"
|
|
|
3521
3226
|
}
|
|
3522
3227
|
var GENERIC_SKILL_MD = `---
|
|
3523
3228
|
name: atomix-ds
|
|
3524
|
-
description: Use the
|
|
3229
|
+
description: Use the project's design system tokens semantically when building or editing any UI. Covers everyday tasks \u2014 creating components, building pages, styling forms, theming, dark mode, responsive layouts, adding icons, choosing colors, setting type, adjusting spacing, and applying motion. Works with any platform or framework (web, React Native, iOS, Android, Flutter, etc.). Fetch token values and governance rules via MCP tools (getToken, listTokens, searchTokens, getRules, validateUsage). Tokens include colors, typography, spacing, sizing, radius, shadows, borders, motion, z-index, and icons. Never hardcode visual values \u2014 always resolve from the design system first.
|
|
3525
3230
|
---
|
|
3526
3231
|
|
|
3527
3232
|
# Atomix Design System
|
|
3528
3233
|
|
|
3529
|
-
|
|
3234
|
+
**Prerequisite:** Load rules first before anything else \u2014 call getRules() at the start of the session or before writing any visual code.
|
|
3530
3235
|
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
Complete the user's task using the design system as the single source of truth. Every color, spacing, typography, radius, shadow, or sizing value must come from the MCP tools (tokens or governance rules). Do not hardcode hex codes, pixel values, or font names.
|
|
3236
|
+
This project uses a design system with semantic tokens. **Before writing any visual code**, fetch the relevant tokens and governance rules from the MCP tools below. The design system is the single source of truth \u2014 do not guess, invent, or hardcode visual values.
|
|
3534
3237
|
|
|
3535
3238
|
## When to use
|
|
3536
3239
|
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
-
|
|
3240
|
+
Use this skill for everyday design and frontend tasks:
|
|
3241
|
+
|
|
3242
|
+
**Building UI**
|
|
3243
|
+
- Creating a component (button, card, input, modal, nav, sidebar, table, list, badge, tooltip\u2026)
|
|
3244
|
+
- Laying out a page, screen, or view
|
|
3245
|
+
- Building a form, dialog, popover, or overlay
|
|
3246
|
+
- Adding a header, footer, hero section, or onboarding flow
|
|
3247
|
+
|
|
3248
|
+
**Styling & theming**
|
|
3249
|
+
- Choosing colors for backgrounds, text, borders, icons, or interactive states
|
|
3250
|
+
- Setting typography \u2014 font family, size, weight, line height, letter spacing
|
|
3251
|
+
- Adjusting spacing, padding, margins, or gaps between elements
|
|
3252
|
+
- Applying border radius, shadows, or elevation to surfaces
|
|
3253
|
+
- Adding hover, focus, active, disabled, or loading states
|
|
3254
|
+
- Setting up dark mode, light mode, or theme switching
|
|
3255
|
+
- Configuring responsive or adaptive layouts
|
|
3256
|
+
|
|
3257
|
+
**Working with assets**
|
|
3258
|
+
- Rendering icons (sizing and stroke width are defined by the design system)
|
|
3259
|
+
- Implementing designs from Figma, mockups, screenshots, or design specs
|
|
3260
|
+
- Translating a design handoff into code
|
|
3261
|
+
|
|
3262
|
+
**Maintenance**
|
|
3263
|
+
- Refactoring hardcoded values to use tokens
|
|
3264
|
+
- Auditing code for design system compliance
|
|
3265
|
+
- Updating styles after a design system version change
|
|
3266
|
+
|
|
3267
|
+
If the task has no visual output (pure logic, data, APIs, DevOps), this skill is not needed.
|
|
3268
|
+
|
|
3269
|
+
## Semantic token usage
|
|
3270
|
+
|
|
3271
|
+
Tokens have two layers \u2014 **primitives** (raw scales) and **semantic** (purpose-driven). Always prefer semantic tokens because they adapt to themes and modes automatically.
|
|
3272
|
+
|
|
3273
|
+
| Intent | Use semantic token | Avoid raw primitive |
|
|
3274
|
+
|--------|-------------------|---------------------|
|
|
3275
|
+
| Page background | \`bg-page\` | \`neutral.50\` |
|
|
3276
|
+
| Card surface | \`bg-surface\` | \`white\` |
|
|
3277
|
+
| Primary text | \`text-primary\` | \`neutral.900\` |
|
|
3278
|
+
| Muted text | \`text-muted\` | \`neutral.500\` |
|
|
3279
|
+
| Default border | \`border-default\` | \`neutral.200\` |
|
|
3280
|
+
| Brand action | \`brand.primary\` | \`green.600\` |
|
|
3281
|
+
| Error state | \`status.error\` | \`red.500\` |
|
|
3282
|
+
|
|
3283
|
+
Call \`getRules\` for the full mapping of semantic tokens to primitives in each theme.
|
|
3284
|
+
|
|
3285
|
+
When a semantic token doesn't exist for your use case, use the closest primitive from \`listTokens\` \u2014 but document why so the team can promote it to a semantic token later.
|
|
3286
|
+
|
|
3287
|
+
## How to fetch design system data
|
|
3288
|
+
|
|
3289
|
+
### 1. Governance rules \u2014 always fetch first
|
|
3541
3290
|
|
|
3542
|
-
|
|
3291
|
+
\`getRules()\` \u2014 optionally with a topic: \`colors\`, \`typo\`, \`motion\`, \`icons\`, \`layout\`, or \`visual\` (color, border, radius, shadows, icons). Returns how tokens should be applied \u2014 naming conventions, variable format, and semantic mappings.
|
|
3543
3292
|
|
|
3544
|
-
|
|
3545
|
-
Call **getAIToolRules** with the tool id for your current environment: \`cursor\`, \`windsurf\`, \`copilot\`, \`cline\`, \`continue\`, \`zed\`, or \`generic\`.
|
|
3546
|
-
Example: \`getAIToolRules({ tool: "cursor" })\`.
|
|
3547
|
-
Alternatively use the **/--rules** prompt or the resource \`atomix://rules/<tool>\`.
|
|
3293
|
+
### 2. Token values \u2014 by task
|
|
3548
3294
|
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3295
|
+
| I need\u2026 | MCP call |
|
|
3296
|
+
|---------|----------|
|
|
3297
|
+
| A specific token | \`getToken("colors.brand.primary")\` or \`getToken("spacing.scale.md")\` |
|
|
3298
|
+
| All tokens in a category | \`listTokens("colors")\` \u2014 categories: \`colors\`, \`typography\`, \`spacing\`, \`sizing\`, \`shadows\`, \`radius\`, \`borders\`, \`motion\`, \`zIndex\` |
|
|
3299
|
+
| Search by name or value | \`searchTokens("primary")\` or \`searchTokens("bold")\` |
|
|
3300
|
+
| Icon size or stroke | \`getToken("sizing.icon.sm")\` for dimensions, \`getToken("icons.strokeWidth")\` for stroke |
|
|
3301
|
+
| Typeset classes | \`listTypesets()\` \u2014 emit one class per typeset; include text-transform and text-decoration for 1:1 match |
|
|
3553
3302
|
|
|
3554
|
-
|
|
3555
|
-
- **validateUsage(value, context)** \u2014 Check if a CSS/value should use a token instead (e.g. \`validateUsage("#007061", "color")\`).
|
|
3303
|
+
### 3. Validation
|
|
3556
3304
|
|
|
3557
|
-
|
|
3558
|
-
- **syncAll({ output?, format?, skipTokens? })** \u2014 Syncs tokens to a file, AI rules, skills (.cursor/skills/atomix-ds/*), and atomix-dependencies.json. Default output \`./tokens.css\`, format \`css\`. Use \`skipTokens: true\` to only write skills and manifest.
|
|
3305
|
+
\`validateUsage("#007061", "color")\` \u2014 checks if a raw value should be a token. Run this on any value you suspect is hardcoded.
|
|
3559
3306
|
|
|
3560
|
-
|
|
3307
|
+
### 4. Syncing tokens to a file
|
|
3308
|
+
|
|
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.
|
|
3310
|
+
|
|
3311
|
+
## Workflow
|
|
3312
|
+
|
|
3313
|
+
1. **Fetch rules** \u2014 call \`getRules\` once per session (or with a topic when working on a specific area).
|
|
3314
|
+
2. **Fetch tokens** \u2014 call \`getToken\`, \`listTokens\`, or \`searchTokens\` for the values you need.
|
|
3315
|
+
3. **Apply semantically** \u2014 use token references or CSS variables (\`var(--atmx-*)\`) depending on your platform. Choose the semantic token that matches the *purpose*, not just the visual appearance.
|
|
3316
|
+
4. **Self-check** \u2014 scan your output for hardcoded hex codes, pixel values, rem/em literals, duration strings, or font names. If found, replace with the matching token.
|
|
3317
|
+
|
|
3318
|
+
## Common mistakes
|
|
3319
|
+
|
|
3320
|
+
Do not hardcode visual values. Always resolve from the design system:
|
|
3321
|
+
|
|
3322
|
+
- A hex color (\`#007061\`, \`#333\`) \u2192 call \`getToken\` or \`searchTokens\` for the matching color token
|
|
3323
|
+
- A pixel/rem value (\`16px\`, \`1.5rem\`) \u2192 use the spacing, sizing, or radius token
|
|
3324
|
+
- A font name (\`"Inter"\`, \`"SF Pro"\`) \u2192 use the typography font-family token
|
|
3325
|
+
- A duration (\`200ms\`, \`0.3s\`) \u2192 use the motion duration token
|
|
3326
|
+
- A numeric weight (\`600\`, \`700\`) \u2192 use the typography font-weight token
|
|
3327
|
+
- A shadow string (\`0 4px 6px rgba(\u2026)\`) \u2192 use the shadow elevation token
|
|
3328
|
+
|
|
3329
|
+
If no token matches, call \`searchTokens\` to find the closest option. Never invent a token path.
|
|
3561
3330
|
|
|
3562
3331
|
## Best practices
|
|
3563
3332
|
|
|
3564
|
-
- **Fetch first:**
|
|
3565
|
-
- **
|
|
3566
|
-
- **
|
|
3567
|
-
- **
|
|
3568
|
-
- **
|
|
3333
|
+
- **Fetch first:** Always call getRules and/or listTokens before writing any styles, regardless of platform or framework.
|
|
3334
|
+
- **Semantic over primitive:** Prefer tokens that describe purpose (\`text-primary\`, \`bg-surface\`) over tokens that describe appearance (\`neutral.900\`, \`white\`).
|
|
3335
|
+
- **Icons:** Size via \`getToken("sizing.icon.sm")\`; stroke width via \`getToken("icons.strokeWidth")\` when the DS defines it.
|
|
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.
|
|
3337
|
+
- **No guessing:** If a value is not in the rules or token list, call searchTokens or listTokens to find the closest match.
|
|
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.
|
|
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.
|
|
3340
|
+
|
|
3341
|
+
## Strict mode and patterns
|
|
3342
|
+
|
|
3343
|
+
- **Strict mode:** NO arbitrary values (e.g. \`bg-[#ff0000]\` forbidden). NO hardcoded colors \u2014 use CSS variables and semantic tokens only. NO hardcoded pixel values \u2014 use spacing/sizing tokens only. NO hardcoded typography \u2014 use typography typeset tokens only. Token vocabulary only \u2014 if a value is not in the design system, do not use it.
|
|
3344
|
+
- **CSS variables:** All tokens follow \`--atmx-{category}-{subcategory}-{token}\`. Correct: \`backgroundColor: "var(--atmx-color-bg-surface)"\`, \`borderRadius: "var(--atmx-radius-scale-md)"\`. Wrong: hex, raw px.
|
|
3345
|
+
- **Dark mode:** Colors switch in dark mode when using CSS variables; \`.dark\` on root toggles color variables.
|
|
3346
|
+
- **Button pattern:** Height and padding use sizing/spacing tokens; typography from typeset tokens; radius and border from tokens; transition from motion tokens; primary/secondary/ghost use action color tokens (e.g. \`--atmx-color-action-primary\`, \`--atmx-color-action-on-primary\`).
|
|
3569
3347
|
`;
|
|
3570
3348
|
var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
|
|
3571
3349
|
<html lang="en">
|
|
@@ -3581,52 +3359,153 @@ var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
|
|
|
3581
3359
|
body {
|
|
3582
3360
|
margin: 0;
|
|
3583
3361
|
font-family: {{FONT_FAMILY_VAR}}, system-ui, sans-serif;
|
|
3584
|
-
background:
|
|
3585
|
-
color:
|
|
3362
|
+
background: var(--atmx-color-bg-page);
|
|
3363
|
+
color: var(--atmx-color-text-primary);
|
|
3586
3364
|
min-height: 100vh;
|
|
3587
|
-
padding:
|
|
3365
|
+
padding: var(--atmx-spacing-scale-2xl);
|
|
3588
3366
|
display: flex;
|
|
3589
3367
|
justify-content: center;
|
|
3590
3368
|
align-items: center;
|
|
3591
3369
|
}
|
|
3592
3370
|
.wrap { width: 375px; max-width: 100%; }
|
|
3593
|
-
.
|
|
3371
|
+
.top-row {
|
|
3372
|
+
display: flex;
|
|
3373
|
+
justify-content: space-between;
|
|
3374
|
+
align-items: flex-start;
|
|
3375
|
+
margin-bottom: var(--atmx-spacing-scale-lg);
|
|
3376
|
+
}
|
|
3377
|
+
.top-row .mode-toggle { margin-bottom: 0; }
|
|
3378
|
+
.icon-circle {
|
|
3379
|
+
display: inline-flex;
|
|
3380
|
+
align-items: center;
|
|
3381
|
+
justify-content: center;
|
|
3382
|
+
width: calc({{ICON_SIZE_VAR}} + 2 * {{CIRCLE_PADDING_VAR}});
|
|
3383
|
+
height: calc({{ICON_SIZE_VAR}} + 2 * {{CIRCLE_PADDING_VAR}});
|
|
3384
|
+
padding: {{CIRCLE_PADDING_VAR}};
|
|
3385
|
+
background: {{BRAND_PRIMARY_VAR}};
|
|
3386
|
+
border-radius: 50%;
|
|
3387
|
+
}
|
|
3388
|
+
.icon-circle.light-icon { color: #fff; }
|
|
3389
|
+
.icon-circle.dark-icon { color: #000; }
|
|
3390
|
+
.icon-circle svg {
|
|
3391
|
+
width: {{ICON_SIZE_VAR}};
|
|
3392
|
+
height: {{ICON_SIZE_VAR}};
|
|
3393
|
+
flex-shrink: 0;
|
|
3394
|
+
}
|
|
3395
|
+
.mode-toggle {
|
|
3396
|
+
display: inline-flex;
|
|
3397
|
+
align-items: center;
|
|
3398
|
+
justify-content: center;
|
|
3399
|
+
padding: {{BUTTON_PADDING_VAR}};
|
|
3400
|
+
height: {{BUTTON_HEIGHT_VAR}};
|
|
3401
|
+
border-radius: {{BUTTON_RADIUS_VAR}};
|
|
3402
|
+
background: var(--atmx-color-bg-surface);
|
|
3403
|
+
color: var(--atmx-color-text-primary);
|
|
3404
|
+
border: 1px solid var(--atmx-color-border-default);
|
|
3405
|
+
font-family: inherit;
|
|
3406
|
+
font-size: inherit;
|
|
3407
|
+
font-weight: inherit;
|
|
3408
|
+
cursor: pointer;
|
|
3409
|
+
margin-bottom: var(--atmx-spacing-scale-xl);
|
|
3410
|
+
}
|
|
3411
|
+
.mode-toggle:hover {
|
|
3412
|
+
background: var(--atmx-color-bg-muted);
|
|
3413
|
+
}
|
|
3594
3414
|
h1 {
|
|
3595
3415
|
font-family: {{HEADING_FONT_VAR}}, {{FONT_FAMILY_VAR}}, system-ui, sans-serif;
|
|
3596
|
-
|
|
3597
|
-
font-weight: 700;
|
|
3598
|
-
margin: 0 0 0.75rem;
|
|
3416
|
+
margin: 0 0 var(--atmx-spacing-scale-md);
|
|
3599
3417
|
line-height: 1.2;
|
|
3600
3418
|
}
|
|
3601
|
-
.lead {
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
.
|
|
3419
|
+
.lead {
|
|
3420
|
+
margin: 0 0 var(--atmx-spacing-scale-xl);
|
|
3421
|
+
opacity: 0.95;
|
|
3422
|
+
}
|
|
3423
|
+
.now { margin: var(--atmx-spacing-scale-xl) 0 0; opacity: 0.95; text-align: left; }
|
|
3424
|
+
.now strong { display: block; margin-bottom: var(--atmx-spacing-scale-sm); }
|
|
3425
|
+
.now ul { margin: 0; padding-left: var(--atmx-spacing-scale-xl); }
|
|
3426
|
+
.tips { margin-top: var(--atmx-spacing-scale-xl); opacity: 0.9; }
|
|
3606
3427
|
.tips a { color: inherit; text-decoration: underline; }
|
|
3428
|
+
code {
|
|
3429
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
3430
|
+
font-size: 0.8125rem;
|
|
3431
|
+
padding: 0.125rem 0.375rem;
|
|
3432
|
+
border-radius: 0.25rem;
|
|
3433
|
+
background: var(--atmx-color-bg-muted);
|
|
3434
|
+
}
|
|
3607
3435
|
</style>
|
|
3608
3436
|
</head>
|
|
3609
3437
|
<body class="{{BODY_TYPESET_CLASS}}">
|
|
3610
3438
|
<div class="wrap">
|
|
3611
|
-
<div class="
|
|
3612
|
-
<
|
|
3439
|
+
<div class="top-row">
|
|
3440
|
+
<div class="icon-circle" aria-hidden="true">
|
|
3441
|
+
{{CHECK_ICON_SVG}}
|
|
3442
|
+
</div>
|
|
3443
|
+
<button type="button" class="mode-toggle" id="mode-toggle" aria-label="Toggle light or dark mode">Dark</button>
|
|
3613
3444
|
</div>
|
|
3614
3445
|
<h1 class="{{LARGEST_DISPLAY_TYPESET_CLASS}}">You're all set with {{DS_NAME}}</h1>
|
|
3615
|
-
<p class="lead">This page uses your design system:
|
|
3616
|
-
<div class="now">
|
|
3446
|
+
<p class="lead {{LARGEST_BODY_TYPESET_CLASS}}">This page uses your design system: semantic colors (mode-aware), headline and body typesets, and an icon.</p>
|
|
3447
|
+
<div class="now {{LARGEST_BODY_TYPESET_CLASS}}">
|
|
3617
3448
|
<strong>What you can do now:</strong>
|
|
3618
3449
|
<ul>
|
|
3619
3450
|
<li>Ask your agent to build your designs using the design system tokens</li>
|
|
3620
3451
|
<li>Build components and pages that use <code>var(--atmx-*)</code> for colors, spacing, and typography</li>
|
|
3621
|
-
<li>Run <code>/--rules</code> to load governance rules; run <code>/--sync</code> and <code>/--refactor</code> after you change tokens in Atomix Studio</li>
|
|
3452
|
+
<li>Run <code>/--rules</code> to load governance rules (or call getRules); run <code>/--sync</code> and <code>/--refactor</code> after you change tokens in Atomix Studio</li>
|
|
3622
3453
|
</ul>
|
|
3623
3454
|
</div>
|
|
3624
|
-
<p class="tips">Keep the source of truth at <a href="https://atomix.studio" target="_blank" rel="noopener">atomix.studio</a> \u2014 avoid editing token values in this repo.</p>
|
|
3455
|
+
<p class="tips {{LARGEST_BODY_TYPESET_CLASS}}">Keep the source of truth at <a href="https://atomix.studio" target="_blank" rel="noopener">atomix.studio</a> \u2014 avoid editing token values in this repo.</p>
|
|
3625
3456
|
</div>
|
|
3457
|
+
<script>
|
|
3458
|
+
(function() {
|
|
3459
|
+
var root = document.documentElement;
|
|
3460
|
+
var btn = document.getElementById('mode-toggle');
|
|
3461
|
+
var circle = document.querySelector('.icon-circle');
|
|
3462
|
+
function hexToRgb(hex) {
|
|
3463
|
+
var m = hex.slice(1).match(/.{2}/g);
|
|
3464
|
+
return m ? m.map(function(x) { return parseInt(x, 16) / 255; }) : [0, 0, 0];
|
|
3465
|
+
}
|
|
3466
|
+
function relativeLuminance(r, g, b) {
|
|
3467
|
+
var srgb = function(x) { return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); };
|
|
3468
|
+
return 0.2126 * srgb(r) + 0.7152 * srgb(g) + 0.0722 * srgb(b);
|
|
3469
|
+
}
|
|
3470
|
+
function setIconContrast() {
|
|
3471
|
+
if (!circle) return;
|
|
3472
|
+
var bg = getComputedStyle(circle).backgroundColor;
|
|
3473
|
+
var r = 0, g = 0, b = 0;
|
|
3474
|
+
var rgbMatch = bg.match(/rgb\\(?\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)?/);
|
|
3475
|
+
if (rgbMatch) {
|
|
3476
|
+
r = parseInt(rgbMatch[1], 10) / 255;
|
|
3477
|
+
g = parseInt(rgbMatch[2], 10) / 255;
|
|
3478
|
+
b = parseInt(rgbMatch[3], 10) / 255;
|
|
3479
|
+
} else if (bg.indexOf('#') === 0) {
|
|
3480
|
+
var parts = hexToRgb(bg);
|
|
3481
|
+
r = parts[0]; g = parts[1]; b = parts[2];
|
|
3482
|
+
} else return;
|
|
3483
|
+
var L = relativeLuminance(r, g, b);
|
|
3484
|
+
circle.classList.remove('light-icon', 'dark-icon');
|
|
3485
|
+
circle.classList.add(L > 0.179 ? 'dark-icon' : 'light-icon');
|
|
3486
|
+
}
|
|
3487
|
+
function updateLabel() {
|
|
3488
|
+
var isDark = root.classList.contains('dark');
|
|
3489
|
+
btn.textContent = isDark ? 'Light' : 'Dark';
|
|
3490
|
+
btn.setAttribute('aria-label', isDark ? 'Switch to light mode' : 'Switch to dark mode');
|
|
3491
|
+
}
|
|
3492
|
+
btn.addEventListener('click', function() {
|
|
3493
|
+
root.classList.toggle('dark');
|
|
3494
|
+
root.setAttribute('data-theme', root.classList.contains('dark') ? 'dark' : 'light');
|
|
3495
|
+
updateLabel();
|
|
3496
|
+
setIconContrast();
|
|
3497
|
+
});
|
|
3498
|
+
if (root.classList.contains('dark') || root.getAttribute('data-theme') === 'dark') {
|
|
3499
|
+
root.classList.add('dark');
|
|
3500
|
+
root.setAttribute('data-theme', 'dark');
|
|
3501
|
+
}
|
|
3502
|
+
updateLabel();
|
|
3503
|
+
setIconContrast();
|
|
3504
|
+
})();
|
|
3505
|
+
</script>
|
|
3626
3506
|
</body>
|
|
3627
3507
|
</html>
|
|
3628
3508
|
`;
|
|
3629
|
-
var AI_TOOLS = ["cursor", "copilot", "windsurf", "cline", "continue", "zed", "generic"];
|
|
3630
3509
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
3631
3510
|
if (!hasValidAuthConfig() || authFailedNoTools) {
|
|
3632
3511
|
throw new Error(AUTH_REQUIRED_MESSAGE);
|
|
@@ -3681,26 +3560,44 @@ Get your DS ID and token from the Export modal or Settings \u2192 Regenerate Ato
|
|
|
3681
3560
|
}]
|
|
3682
3561
|
};
|
|
3683
3562
|
}
|
|
3684
|
-
const rulesMatch = uri.match(/^atomix:\/\/rules
|
|
3563
|
+
const rulesMatch = uri.match(/^atomix:\/\/rules(?:\/(.+))?$/);
|
|
3685
3564
|
if (rulesMatch) {
|
|
3686
|
-
const
|
|
3687
|
-
|
|
3688
|
-
throw new Error(`Unknown tool: ${tool}. Available: ${AI_TOOLS.join(", ")}`);
|
|
3689
|
-
}
|
|
3690
|
-
const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=${tool}`;
|
|
3565
|
+
const topicRaw = rulesMatch[1]?.toLowerCase().trim();
|
|
3566
|
+
const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
|
|
3691
3567
|
const headers = { "Content-Type": "application/json" };
|
|
3692
3568
|
if (apiKey) headers["x-api-key"] = apiKey;
|
|
3693
3569
|
const response = await fetch(rulesUrl, { headers });
|
|
3694
|
-
if (!response.ok) {
|
|
3695
|
-
|
|
3570
|
+
if (!response.ok) throw new Error(`Failed to fetch rules: ${response.status}`);
|
|
3571
|
+
const payload = await response.json();
|
|
3572
|
+
const categories = payload.categories ?? {};
|
|
3573
|
+
const allRules = payload.rules ?? [];
|
|
3574
|
+
const topicToCategories = {
|
|
3575
|
+
colors: ["general", "colors"],
|
|
3576
|
+
typo: ["general", "typography"],
|
|
3577
|
+
typography: ["general", "typography"],
|
|
3578
|
+
motion: ["general", "motion"],
|
|
3579
|
+
icons: ["general", "icons"],
|
|
3580
|
+
layout: ["general", "spacing", "sizing", "layout"],
|
|
3581
|
+
visual: ["general", "colors", "borders", "radius", "shadows", "icons"],
|
|
3582
|
+
style: ["general", "colors", "borders", "radius", "shadows", "icons"]
|
|
3583
|
+
};
|
|
3584
|
+
if (!topicRaw || !topicToCategories[topicRaw]) {
|
|
3585
|
+
return {
|
|
3586
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ rules: allRules, categories }, null, 2) }]
|
|
3587
|
+
};
|
|
3588
|
+
}
|
|
3589
|
+
const categoryKeys = topicToCategories[topicRaw];
|
|
3590
|
+
const filteredCategories = {};
|
|
3591
|
+
const filteredRules = [];
|
|
3592
|
+
for (const key of categoryKeys) {
|
|
3593
|
+
const list = categories[key];
|
|
3594
|
+
if (list?.length) {
|
|
3595
|
+
filteredCategories[key] = list;
|
|
3596
|
+
filteredRules.push(...list);
|
|
3597
|
+
}
|
|
3696
3598
|
}
|
|
3697
|
-
const rulesData = await response.json();
|
|
3698
3599
|
return {
|
|
3699
|
-
contents: [{
|
|
3700
|
-
uri,
|
|
3701
|
-
mimeType: "text/markdown",
|
|
3702
|
-
text: rulesData.content || JSON.stringify(rulesData, null, 2)
|
|
3703
|
-
}]
|
|
3600
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ rules: filteredRules, categories: filteredCategories }, null, 2) }]
|
|
3704
3601
|
};
|
|
3705
3602
|
}
|
|
3706
3603
|
throw new Error(`Unknown resource: ${uri}`);
|
|
@@ -3723,11 +3620,14 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
|
3723
3620
|
const prompts = [
|
|
3724
3621
|
{ name: "--hello", description: "Get started with this design system - overview, tokens, and tools. Run this first!" },
|
|
3725
3622
|
{ name: "--get-started", description: "Get started with design system in project. Three phases: scan, report and ask, then create only after you approve." },
|
|
3726
|
-
{ name: "--rules", description: "Get
|
|
3623
|
+
{ name: "--rules", description: "Get design system governance rules (optionally by topic: colors, typo, motion, icons, layout, visual)." },
|
|
3727
3624
|
{ name: "--sync", description: "Sync tokens, AI rules, skills files, and dependencies manifest (icons, fonts). Use /--refactor to migrate deprecated tokens." },
|
|
3728
3625
|
{ name: "--refactor", description: "Migrate deprecated tokens in codebase. Run after /--sync." },
|
|
3729
3626
|
{ name: "--sync-to-figma", description: "Push this design system to Figma (variables, color + typography styles). Uses local bridge + plugin; no Figma token." }
|
|
3730
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
|
+
}
|
|
3731
3631
|
return { prompts };
|
|
3732
3632
|
});
|
|
3733
3633
|
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
@@ -3744,8 +3644,18 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
3744
3644
|
}]
|
|
3745
3645
|
};
|
|
3746
3646
|
}
|
|
3747
|
-
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;
|
|
3748
|
-
const shouldForceRefresh =
|
|
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;
|
|
3648
|
+
const shouldForceRefresh = [
|
|
3649
|
+
"hello",
|
|
3650
|
+
"atomix-setup",
|
|
3651
|
+
// --get-started
|
|
3652
|
+
"design-system-rules",
|
|
3653
|
+
// --rules
|
|
3654
|
+
"sync",
|
|
3655
|
+
"refactor",
|
|
3656
|
+
"sync-to-figma",
|
|
3657
|
+
"design-in-figma"
|
|
3658
|
+
].includes(canonicalName);
|
|
3749
3659
|
let data = null;
|
|
3750
3660
|
let stats = null;
|
|
3751
3661
|
try {
|
|
@@ -3848,6 +3758,20 @@ Both are required. Configure the MCP server in your AI tool's MCP settings, then
|
|
|
3848
3758
|
lines.push(instructions);
|
|
3849
3759
|
return lines.join("\n");
|
|
3850
3760
|
};
|
|
3761
|
+
function withMcpNotice(res) {
|
|
3762
|
+
if (!mcpUpdateNotice || res.messages.length === 0) return res;
|
|
3763
|
+
const first = res.messages[0];
|
|
3764
|
+
if (first?.content?.type === "text" && typeof first.content.text === "string") {
|
|
3765
|
+
return {
|
|
3766
|
+
...res,
|
|
3767
|
+
messages: [
|
|
3768
|
+
{ ...first, content: { ...first.content, text: first.content.text + "\n\n---\n\n" + mcpUpdateNotice } },
|
|
3769
|
+
...res.messages.slice(1)
|
|
3770
|
+
]
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
return res;
|
|
3774
|
+
}
|
|
3851
3775
|
switch (canonicalName) {
|
|
3852
3776
|
case "hello": {
|
|
3853
3777
|
const welcome = generateWelcomeMessage(data, stats);
|
|
@@ -3860,7 +3784,7 @@ Do not add any introduction or commentary before the ASCII art. The ASCII art mu
|
|
|
3860
3784
|
|
|
3861
3785
|
---
|
|
3862
3786
|
${welcome}`;
|
|
3863
|
-
return {
|
|
3787
|
+
return withMcpNotice({
|
|
3864
3788
|
description: `Hello \u2014 ${data.meta.name} Design System`,
|
|
3865
3789
|
messages: [
|
|
3866
3790
|
{
|
|
@@ -3871,41 +3795,55 @@ ${welcome}`;
|
|
|
3871
3795
|
}
|
|
3872
3796
|
}
|
|
3873
3797
|
]
|
|
3874
|
-
};
|
|
3798
|
+
});
|
|
3875
3799
|
}
|
|
3876
3800
|
case "design-system-rules": {
|
|
3877
|
-
const
|
|
3878
|
-
const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format
|
|
3801
|
+
const topic = args?.topic?.toLowerCase().trim();
|
|
3802
|
+
const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
|
|
3879
3803
|
const headers = { "Content-Type": "application/json" };
|
|
3880
3804
|
if (apiKey) headers["x-api-key"] = apiKey;
|
|
3881
3805
|
const response = await fetch(rulesUrl, { headers });
|
|
3882
|
-
if (!response.ok) {
|
|
3883
|
-
|
|
3806
|
+
if (!response.ok) throw new Error(`Failed to fetch rules: ${response.status}`);
|
|
3807
|
+
const payload = await response.json();
|
|
3808
|
+
const categories = payload.categories ?? {};
|
|
3809
|
+
const allRules = payload.rules ?? [];
|
|
3810
|
+
const topicToCategories = {
|
|
3811
|
+
colors: ["general", "colors"],
|
|
3812
|
+
typo: ["general", "typography"],
|
|
3813
|
+
typography: ["general", "typography"],
|
|
3814
|
+
motion: ["general", "motion"],
|
|
3815
|
+
icons: ["general", "icons"],
|
|
3816
|
+
layout: ["general", "spacing", "sizing", "layout"],
|
|
3817
|
+
visual: ["general", "colors", "borders", "radius", "shadows", "icons"],
|
|
3818
|
+
style: ["general", "colors", "borders", "radius", "shadows", "icons"]
|
|
3819
|
+
};
|
|
3820
|
+
let rulesText;
|
|
3821
|
+
if (topic && topicToCategories[topic]) {
|
|
3822
|
+
const categoryKeys = topicToCategories[topic];
|
|
3823
|
+
const filteredCategories = {};
|
|
3824
|
+
const filteredRules = [];
|
|
3825
|
+
for (const key of categoryKeys) {
|
|
3826
|
+
const list = categories[key];
|
|
3827
|
+
if (list?.length) {
|
|
3828
|
+
filteredCategories[key] = list;
|
|
3829
|
+
filteredRules.push(...list);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
rulesText = JSON.stringify({ rules: filteredRules, categories: filteredCategories }, null, 2);
|
|
3833
|
+
} else {
|
|
3834
|
+
rulesText = JSON.stringify({ rules: allRules, categories }, null, 2);
|
|
3884
3835
|
}
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
description: `Design system rules for ${tool}`,
|
|
3836
|
+
return withMcpNotice({
|
|
3837
|
+
description: topic ? `Design system rules (topic: ${topic})` : "Design system rules",
|
|
3888
3838
|
messages: [
|
|
3889
|
-
{
|
|
3890
|
-
|
|
3891
|
-
content: {
|
|
3892
|
-
type: "text",
|
|
3893
|
-
text: `Show me the design system rules for ${tool}.`
|
|
3894
|
-
}
|
|
3895
|
-
},
|
|
3896
|
-
{
|
|
3897
|
-
role: "assistant",
|
|
3898
|
-
content: {
|
|
3899
|
-
type: "text",
|
|
3900
|
-
text: rulesData.content || JSON.stringify(rulesData, null, 2)
|
|
3901
|
-
}
|
|
3902
|
-
}
|
|
3839
|
+
{ role: "user", content: { type: "text", text: topic ? `Show me the design system rules for topic: ${topic}.` : "Show me the design system rules." } },
|
|
3840
|
+
{ role: "assistant", content: { type: "text", text: rulesText } }
|
|
3903
3841
|
]
|
|
3904
|
-
};
|
|
3842
|
+
});
|
|
3905
3843
|
}
|
|
3906
3844
|
case "spacing": {
|
|
3907
3845
|
const instructions = `List all spacing tokens in a table format. Use the listTokens tool with category "spacing" and subcategory "scale". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "spacing.xs" instead of "spacing.scale.xs").`;
|
|
3908
|
-
|
|
3846
|
+
return withMcpNotice({
|
|
3909
3847
|
description: "List all spacing tokens",
|
|
3910
3848
|
messages: [
|
|
3911
3849
|
{
|
|
@@ -3916,12 +3854,11 @@ ${welcome}`;
|
|
|
3916
3854
|
}
|
|
3917
3855
|
}
|
|
3918
3856
|
]
|
|
3919
|
-
};
|
|
3920
|
-
return response;
|
|
3857
|
+
});
|
|
3921
3858
|
}
|
|
3922
3859
|
case "radius": {
|
|
3923
3860
|
const instructions = `List all border radius tokens in a table format. Use the listTokens tool with category "radius" and subcategory "scale". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "radius.sm" instead of "radius.scale.sm").`;
|
|
3924
|
-
return {
|
|
3861
|
+
return withMcpNotice({
|
|
3925
3862
|
description: "List all border radius tokens",
|
|
3926
3863
|
messages: [
|
|
3927
3864
|
{
|
|
@@ -3932,7 +3869,7 @@ ${welcome}`;
|
|
|
3932
3869
|
}
|
|
3933
3870
|
}
|
|
3934
3871
|
]
|
|
3935
|
-
};
|
|
3872
|
+
});
|
|
3936
3873
|
}
|
|
3937
3874
|
case "color": {
|
|
3938
3875
|
const instructions = `List all color tokens in a table format showing both light and dark mode values.
|
|
@@ -3949,7 +3886,7 @@ For semantic colors (from modes.light/modes.dark), match tokens by name and show
|
|
|
3949
3886
|
The Token Name should be in short format:
|
|
3950
3887
|
- Brand colors: "colors.brand.primary" (not "colors.static.brand.primary")
|
|
3951
3888
|
- Semantic colors: "colors.bgSurface" (not "colors.modes.light.bgSurface")`;
|
|
3952
|
-
return {
|
|
3889
|
+
return withMcpNotice({
|
|
3953
3890
|
description: "List all color tokens with light/dark mode",
|
|
3954
3891
|
messages: [
|
|
3955
3892
|
{
|
|
@@ -3960,11 +3897,11 @@ The Token Name should be in short format:
|
|
|
3960
3897
|
}
|
|
3961
3898
|
}
|
|
3962
3899
|
]
|
|
3963
|
-
};
|
|
3900
|
+
});
|
|
3964
3901
|
}
|
|
3965
3902
|
case "typography": {
|
|
3966
3903
|
const instructions = `List all typography tokens in a table format. Use the listTokens tool with category "typography" (no subcategory needed). Format the response as a markdown table with columns: Token Name | Value | CSS Variable. Group tokens by type (fontSize, fontWeight, lineHeight, etc.) with section headers.`;
|
|
3967
|
-
return {
|
|
3904
|
+
return withMcpNotice({
|
|
3968
3905
|
description: "List all typography tokens",
|
|
3969
3906
|
messages: [
|
|
3970
3907
|
{
|
|
@@ -3975,11 +3912,11 @@ The Token Name should be in short format:
|
|
|
3975
3912
|
}
|
|
3976
3913
|
}
|
|
3977
3914
|
]
|
|
3978
|
-
};
|
|
3915
|
+
});
|
|
3979
3916
|
}
|
|
3980
3917
|
case "shadow": {
|
|
3981
3918
|
const instructions = `List all shadow/elevation tokens in a table format. Use the listTokens tool with category "shadows" and subcategory "elevation". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "shadows.elevation.md" is fine as-is).`;
|
|
3982
|
-
return {
|
|
3919
|
+
return withMcpNotice({
|
|
3983
3920
|
description: "List all shadow/elevation tokens",
|
|
3984
3921
|
messages: [
|
|
3985
3922
|
{
|
|
@@ -3990,11 +3927,11 @@ The Token Name should be in short format:
|
|
|
3990
3927
|
}
|
|
3991
3928
|
}
|
|
3992
3929
|
]
|
|
3993
|
-
};
|
|
3930
|
+
});
|
|
3994
3931
|
}
|
|
3995
3932
|
case "border": {
|
|
3996
3933
|
const instructions = `List all border width tokens in a table format. Use the listTokens tool with category "borders" and subcategory "width". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "borders.width.sm" is fine as-is).`;
|
|
3997
|
-
return {
|
|
3934
|
+
return withMcpNotice({
|
|
3998
3935
|
description: "List all border width tokens",
|
|
3999
3936
|
messages: [
|
|
4000
3937
|
{
|
|
@@ -4005,7 +3942,7 @@ The Token Name should be in short format:
|
|
|
4005
3942
|
}
|
|
4006
3943
|
}
|
|
4007
3944
|
]
|
|
4008
|
-
};
|
|
3945
|
+
});
|
|
4009
3946
|
}
|
|
4010
3947
|
case "sizing": {
|
|
4011
3948
|
const instructions = `List all sizing tokens in a table format. Call listTokens twice:
|
|
@@ -4013,7 +3950,7 @@ The Token Name should be in short format:
|
|
|
4013
3950
|
2. category "sizing" and subcategory "icon" for icon sizes
|
|
4014
3951
|
|
|
4015
3952
|
Format the response as a markdown table with columns: Token Name | Value | CSS Variable. Group by type (height vs icon) with section headers.`;
|
|
4016
|
-
return {
|
|
3953
|
+
return withMcpNotice({
|
|
4017
3954
|
description: "List all sizing tokens",
|
|
4018
3955
|
messages: [
|
|
4019
3956
|
{
|
|
@@ -4024,7 +3961,7 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
|
|
|
4024
3961
|
}
|
|
4025
3962
|
}
|
|
4026
3963
|
]
|
|
4027
|
-
};
|
|
3964
|
+
});
|
|
4028
3965
|
}
|
|
4029
3966
|
case "motion": {
|
|
4030
3967
|
const instructions = `List all motion tokens in a table format. Call listTokens twice:
|
|
@@ -4032,7 +3969,7 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
|
|
|
4032
3969
|
2. category "motion" and subcategory "easing" for easing tokens
|
|
4033
3970
|
|
|
4034
3971
|
Format the response as a markdown table with columns: Token Name | Value | CSS Variable. Group by type (duration vs easing) with section headers.`;
|
|
4035
|
-
return {
|
|
3972
|
+
return withMcpNotice({
|
|
4036
3973
|
description: "List all motion tokens",
|
|
4037
3974
|
messages: [
|
|
4038
3975
|
{
|
|
@@ -4043,26 +3980,28 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
|
|
|
4043
3980
|
}
|
|
4044
3981
|
}
|
|
4045
3982
|
]
|
|
4046
|
-
};
|
|
3983
|
+
});
|
|
4047
3984
|
}
|
|
4048
3985
|
case "sync": {
|
|
4049
3986
|
const output = args?.output || "./tokens.css";
|
|
4050
3987
|
const format = args?.format || "css";
|
|
4051
|
-
|
|
3988
|
+
const workspaceRoot = args?.workspaceRoot;
|
|
3989
|
+
const rootHint = workspaceRoot ? ` Use workspaceRoot: "${workspaceRoot}" so files are written inside the repo.` : " If the project root is known, pass workspaceRoot with its absolute path so skills and manifest are written inside the repo (committable).";
|
|
3990
|
+
return withMcpNotice({
|
|
4052
3991
|
description: "Sync tokens, rules, skills, and dependencies manifest",
|
|
4053
3992
|
messages: [
|
|
4054
3993
|
{
|
|
4055
3994
|
role: "user",
|
|
4056
3995
|
content: {
|
|
4057
3996
|
type: "text",
|
|
4058
|
-
text: `Call the syncAll tool now. Use output="${output}" and format="${format}"
|
|
3997
|
+
text: `Call the syncAll tool now. Use output="${output}" and format="${format}".${rootHint} This syncs tokens, AI rules, skills (.cursor/skills/atomix-ds/*), and atomix-dependencies.json. Execute immediately - do not search or ask questions.`
|
|
4059
3998
|
}
|
|
4060
3999
|
}
|
|
4061
4000
|
]
|
|
4062
|
-
};
|
|
4001
|
+
});
|
|
4063
4002
|
}
|
|
4064
4003
|
case "sync-to-figma": {
|
|
4065
|
-
return {
|
|
4004
|
+
return withMcpNotice({
|
|
4066
4005
|
description: "Push design system to Figma via MCP tool",
|
|
4067
4006
|
messages: [
|
|
4068
4007
|
{
|
|
@@ -4073,34 +4012,108 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
|
|
|
4073
4012
|
}
|
|
4074
4013
|
}
|
|
4075
4014
|
]
|
|
4076
|
-
};
|
|
4015
|
+
});
|
|
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
|
+
});
|
|
4077
4090
|
}
|
|
4078
4091
|
case "refactor": {
|
|
4079
4092
|
const refactorOutput = args?.output || "./tokens.css";
|
|
4080
4093
|
const refactorFormat = args?.format || "css";
|
|
4081
|
-
const refactorOutputPath =
|
|
4082
|
-
const refactorFileExists =
|
|
4094
|
+
const refactorOutputPath = path.resolve(process.cwd(), refactorOutput);
|
|
4095
|
+
const refactorFileExists = fs.existsSync(refactorOutputPath);
|
|
4083
4096
|
if (!data) {
|
|
4084
|
-
return {
|
|
4097
|
+
return withMcpNotice({
|
|
4085
4098
|
description: "Refactor codebase for deprecated tokens",
|
|
4086
4099
|
messages: [{
|
|
4087
4100
|
role: "user",
|
|
4088
4101
|
content: { type: "text", text: `Failed to fetch design system from DB. Check your --ds-id and --atomix-token configuration.` }
|
|
4089
4102
|
}]
|
|
4090
|
-
};
|
|
4103
|
+
});
|
|
4091
4104
|
}
|
|
4092
4105
|
if (!refactorFileExists) {
|
|
4093
|
-
return {
|
|
4106
|
+
return withMcpNotice({
|
|
4094
4107
|
description: "Refactor codebase for deprecated tokens",
|
|
4095
4108
|
messages: [{
|
|
4096
4109
|
role: "user",
|
|
4097
4110
|
content: { type: "text", text: `No token file found at \`${refactorOutput}\`. Please run \`/--sync\` first to create your token file, then run \`/--refactor\` to scan your codebase for deprecated token usage.` }
|
|
4098
4111
|
}]
|
|
4099
|
-
};
|
|
4112
|
+
});
|
|
4100
4113
|
}
|
|
4101
4114
|
const deprecatedTokens = /* @__PURE__ */ new Map();
|
|
4102
4115
|
if (["css", "scss", "less"].includes(refactorFormat)) {
|
|
4103
|
-
const oldContent =
|
|
4116
|
+
const oldContent = fs.readFileSync(refactorOutputPath, "utf-8");
|
|
4104
4117
|
const oldVarPattern = /(?:^|\n)\s*(?:\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\/\s*)?(--[a-zA-Z0-9-]+):\s*([^;]+);/gm;
|
|
4105
4118
|
let match;
|
|
4106
4119
|
while ((match = oldVarPattern.exec(oldContent)) !== null) {
|
|
@@ -4112,7 +4125,7 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
|
|
|
4112
4125
|
const dsVersion = data.meta.version ?? "?";
|
|
4113
4126
|
const dsExportedAt = data.meta.exportedAt ? new Date(data.meta.exportedAt).toLocaleString() : "N/A";
|
|
4114
4127
|
if (deprecatedTokens.size === 0) {
|
|
4115
|
-
return {
|
|
4128
|
+
return withMcpNotice({
|
|
4116
4129
|
description: "Refactor codebase for deprecated tokens",
|
|
4117
4130
|
messages: [{
|
|
4118
4131
|
role: "user",
|
|
@@ -4120,7 +4133,7 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
|
|
|
4120
4133
|
|
|
4121
4134
|
Your token file \`${refactorOutput}\` is aligned with the design system (v${dsVersion}, exported ${dsExportedAt}). No tokens need migration.` }
|
|
4122
4135
|
}]
|
|
4123
|
-
};
|
|
4136
|
+
});
|
|
4124
4137
|
}
|
|
4125
4138
|
const format = refactorFormat;
|
|
4126
4139
|
const isNativeFormat = ["swift", "kotlin", "dart"].includes(format);
|
|
@@ -4221,8 +4234,8 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
|
|
|
4221
4234
|
|
|
4222
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.
|
|
4223
4236
|
- Call **getDependencies** with \`platform\` and optional \`stack\`. If it fails, tell the user the design system could not be reached and stop.
|
|
4224
|
-
- 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).
|
|
4225
|
-
- 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.
|
|
4226
4239
|
- Do not write, create, or add anything in Phase 1.
|
|
4227
4240
|
|
|
4228
4241
|
## Phase 2 \u2013 Report and ask
|
|
@@ -4235,18 +4248,18 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
|
|
|
4235
4248
|
|
|
4236
4249
|
- Run only when the user has said yes (all or specific items).
|
|
4237
4250
|
- For each approved item:
|
|
4238
|
-
- **
|
|
4239
|
-
- **Token file:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css").
|
|
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.
|
|
4240
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.
|
|
4241
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.
|
|
4242
|
-
- **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
|
|
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).
|
|
4243
4256
|
- Report only what you actually created or updated. Do not claim the token file was added if you did not call syncAll.
|
|
4244
4257
|
- **After reporting \u2013 styles/theme:**
|
|
4245
4258
|
- **Web:** If the project already has at least one CSS file: recommend how to integrate Atomix (e.g. import the synced tokens file, use \`var(--atmx-*)\`). Do not suggest a new global CSS. Only if there is **no** CSS file at all, ask once: "There are no CSS files yet. Do you want me to build a global typeset from the design system?" If yes, create a CSS file that includes: (1) font \`@import\` or document that a font link is needed, and (2) **typeset rules**\u2014call **listTypesets** and 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 for a 1:1 match. Use the CSS variable names returned by listTypesets. The output must not be only a font import; it must define every typeset with every style detail from the design system.
|
|
4246
4259
|
- **iOS/Android:** If the project already has theme/style files: recommend how to integrate Atomix tokens. Do not suggest a new global theme. Only if there is **no** theme/style at all, ask once: "There's no theme/style setup yet. Do you want a minimal token-based theme?" and add only if the user says yes.
|
|
4247
4260
|
|
|
4248
4261
|
Create your todo list first, then Phase 1 (resolve platform/stack, call getDependencies, scan, build lists), then Phase 2 (report and ask). Do not perform Phase 3 until the user replies.`;
|
|
4249
|
-
return {
|
|
4262
|
+
return withMcpNotice({
|
|
4250
4263
|
description: "Get started with design system in project (/--get-started). Create todo list; Phase 1 scan, Phase 2 report and ask, Phase 3 create only after user approval.",
|
|
4251
4264
|
messages: [
|
|
4252
4265
|
{
|
|
@@ -4257,7 +4270,7 @@ Create your todo list first, then Phase 1 (resolve platform/stack, call getDepen
|
|
|
4257
4270
|
}
|
|
4258
4271
|
}
|
|
4259
4272
|
]
|
|
4260
|
-
};
|
|
4273
|
+
});
|
|
4261
4274
|
}
|
|
4262
4275
|
default:
|
|
4263
4276
|
throw new Error(`Unknown prompt: ${name}`);
|
|
@@ -4322,6 +4335,7 @@ ${tokenSummary}
|
|
|
4322
4335
|
| **/--rules** | Governance rules for your AI tool (e.g. Cursor, Copilot, Windsurf). |
|
|
4323
4336
|
| **/--sync** | Sync tokens, rules, skills, and dependencies manifest (icons, fonts). Safe: adds new, updates existing, marks deprecated. |
|
|
4324
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. |
|
|
4325
4339
|
| **/--refactor** | Migrate deprecated tokens in codebase. Run after /--sync. |
|
|
4326
4340
|
|
|
4327
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.
|