canicode 0.7.0 → 0.8.0

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.
@@ -959,6 +959,211 @@ async function loadFromApi(fileKey, nodeId, token) {
959
959
  };
960
960
  }
961
961
 
962
+ // src/core/adapters/tailwind-parser.ts
963
+ var SPACING_SCALE = {
964
+ "0": 0,
965
+ "px": 1,
966
+ "0.5": 2,
967
+ "1": 4,
968
+ "1.5": 6,
969
+ "2": 8,
970
+ "2.5": 10,
971
+ "3": 12,
972
+ "3.5": 14,
973
+ "4": 16,
974
+ "5": 20,
975
+ "6": 24,
976
+ "7": 28,
977
+ "8": 32,
978
+ "9": 36,
979
+ "10": 40,
980
+ "11": 44,
981
+ "12": 48,
982
+ "14": 56,
983
+ "16": 64,
984
+ "20": 80,
985
+ "24": 96,
986
+ "28": 112,
987
+ "32": 128,
988
+ "36": 144,
989
+ "40": 160,
990
+ "44": 176,
991
+ "48": 192,
992
+ "52": 208,
993
+ "56": 224,
994
+ "60": 240,
995
+ "64": 256,
996
+ "72": 288,
997
+ "80": 320,
998
+ "96": 384
999
+ };
1000
+ function resolveSpacing(value) {
1001
+ const arbMatch = value.match(/^\[(\d+(?:\.\d+)?)px\]$/);
1002
+ if (arbMatch?.[1]) return parseFloat(arbMatch[1]);
1003
+ return SPACING_SCALE[value];
1004
+ }
1005
+ function extractStylesFromClasses(classes) {
1006
+ const styles = {};
1007
+ const tokens = classes.split(/\s+/);
1008
+ for (const token of tokens) {
1009
+ if (token === "flex" || token === "flex-row") {
1010
+ styles.layoutMode = "HORIZONTAL";
1011
+ } else if (token === "flex-col") {
1012
+ styles.layoutMode = "VERTICAL";
1013
+ } else if (token === "absolute") {
1014
+ styles.layoutPositioning = "ABSOLUTE";
1015
+ } else if (token === "relative" || token === "static") {
1016
+ styles.layoutPositioning = "AUTO";
1017
+ } else if (token === "w-full") {
1018
+ styles.layoutSizingHorizontal = "FILL";
1019
+ } else if (token === "h-full") {
1020
+ styles.layoutSizingVertical = "FILL";
1021
+ } else if (token === "w-fit") {
1022
+ styles.layoutSizingHorizontal = "HUG";
1023
+ } else if (token === "h-fit") {
1024
+ styles.layoutSizingVertical = "HUG";
1025
+ } else if (token.match(/^w-\[.+\]$/) || token.match(/^w-\d/)) {
1026
+ styles.layoutSizingHorizontal = "FIXED";
1027
+ } else if (token.match(/^h-\[.+\]$/) || token.match(/^h-\d/)) {
1028
+ styles.layoutSizingVertical = "FIXED";
1029
+ } else if (token.startsWith("gap-")) {
1030
+ const val = token.slice(4);
1031
+ const px = resolveSpacing(val);
1032
+ if (px !== void 0) styles.itemSpacing = px;
1033
+ } else if (token.startsWith("p-") && !token.startsWith("pl-") && !token.startsWith("pr-") && !token.startsWith("pt-") && !token.startsWith("pb-") && !token.startsWith("px-") && !token.startsWith("py-")) {
1034
+ const val = token.slice(2);
1035
+ const px = resolveSpacing(val);
1036
+ if (px !== void 0) {
1037
+ styles.paddingLeft = px;
1038
+ styles.paddingRight = px;
1039
+ styles.paddingTop = px;
1040
+ styles.paddingBottom = px;
1041
+ }
1042
+ } else if (token.startsWith("px-")) {
1043
+ const px = resolveSpacing(token.slice(3));
1044
+ if (px !== void 0) {
1045
+ styles.paddingLeft = px;
1046
+ styles.paddingRight = px;
1047
+ }
1048
+ } else if (token.startsWith("py-")) {
1049
+ const px = resolveSpacing(token.slice(3));
1050
+ if (px !== void 0) {
1051
+ styles.paddingTop = px;
1052
+ styles.paddingBottom = px;
1053
+ }
1054
+ } else if (token.startsWith("pl-")) {
1055
+ const px = resolveSpacing(token.slice(3));
1056
+ if (px !== void 0) styles.paddingLeft = px;
1057
+ } else if (token.startsWith("pr-")) {
1058
+ const px = resolveSpacing(token.slice(3));
1059
+ if (px !== void 0) styles.paddingRight = px;
1060
+ } else if (token.startsWith("pt-")) {
1061
+ const px = resolveSpacing(token.slice(3));
1062
+ if (px !== void 0) styles.paddingTop = px;
1063
+ } else if (token.startsWith("pb-")) {
1064
+ const px = resolveSpacing(token.slice(3));
1065
+ if (px !== void 0) styles.paddingBottom = px;
1066
+ } else if (token.startsWith("bg-[")) {
1067
+ const colorMatch = token.match(/^bg-\[(.+)\]$/);
1068
+ if (colorMatch?.[1]) {
1069
+ const color = colorMatch[1];
1070
+ if (!styles.fills) styles.fills = [];
1071
+ if (color.startsWith("var(")) {
1072
+ styles.fills.push({ type: "SOLID", color: {}, boundVariable: color });
1073
+ } else {
1074
+ styles.fills.push({ type: "SOLID", color: parseHexColor(color) });
1075
+ }
1076
+ }
1077
+ } else if (token === "shadow" || token.startsWith("shadow-")) {
1078
+ if (!styles.effects) styles.effects = [];
1079
+ styles.effects.push({ type: "DROP_SHADOW" });
1080
+ }
1081
+ }
1082
+ return styles;
1083
+ }
1084
+ function parseHexColor(hex) {
1085
+ const clean = hex.replace("#", "");
1086
+ if (clean.length === 6 || clean.length === 8) {
1087
+ return {
1088
+ r: parseInt(clean.slice(0, 2), 16) / 255,
1089
+ g: parseInt(clean.slice(2, 4), 16) / 255,
1090
+ b: parseInt(clean.slice(4, 6), 16) / 255,
1091
+ a: clean.length === 8 ? parseInt(clean.slice(6, 8), 16) / 255 : 1
1092
+ };
1093
+ }
1094
+ return { r: 0, g: 0, b: 0, a: 1 };
1095
+ }
1096
+ function extractRootClasses(code) {
1097
+ const match = code.match(/className="([^"]+)"/);
1098
+ return match?.[1] ?? "";
1099
+ }
1100
+ function extractAllClasses(code) {
1101
+ const classes = [];
1102
+ const regex = /className="([^"]+)"/g;
1103
+ let match;
1104
+ while ((match = regex.exec(code)) !== null) {
1105
+ if (match[1]) classes.push(match[1]);
1106
+ }
1107
+ return classes;
1108
+ }
1109
+ function parseDesignContextCode(code) {
1110
+ const rootClasses = extractRootClasses(code);
1111
+ const rootStyles = extractStylesFromClasses(rootClasses);
1112
+ const allClasses = extractAllClasses(code);
1113
+ const aggregatedFills = [...rootStyles.fills ?? []];
1114
+ const aggregatedEffects = [...rootStyles.effects ?? []];
1115
+ for (const cls of allClasses) {
1116
+ const styles = extractStylesFromClasses(cls);
1117
+ if (styles.fills) {
1118
+ for (const fill of styles.fills) aggregatedFills.push(fill);
1119
+ }
1120
+ if (styles.effects) {
1121
+ for (const effect of styles.effects) aggregatedEffects.push(effect);
1122
+ }
1123
+ }
1124
+ return {
1125
+ ...rootStyles,
1126
+ ...aggregatedFills.length > 0 ? { fills: aggregatedFills } : {},
1127
+ ...aggregatedEffects.length > 0 ? { effects: aggregatedEffects } : {}
1128
+ };
1129
+ }
1130
+ function parseCodeHeader(code) {
1131
+ const headerMatch = code.match(/\/\*\s*(.+?)\s*\*\//);
1132
+ if (!headerMatch?.[1]) return {};
1133
+ const header = headerMatch[1];
1134
+ const result = {};
1135
+ const nameParts = header.split(" \u2014 ");
1136
+ if (nameParts[0]) result.name = nameParts[0].trim();
1137
+ const dimMatch = header.match(/(\d+)x(\d+)/);
1138
+ if (dimMatch?.[1] && dimMatch[2]) {
1139
+ result.width = parseInt(dimMatch[1]);
1140
+ result.height = parseInt(dimMatch[2]);
1141
+ }
1142
+ const typeMatch = header.match(/\b(COMPONENT|FRAME|INSTANCE|GROUP|SECTION)\b/);
1143
+ if (typeMatch?.[1]) result.type = typeMatch[1];
1144
+ if (header.includes("no auto-layout")) {
1145
+ result.hasAutoLayout = false;
1146
+ } else if (header.includes("auto-layout")) {
1147
+ result.hasAutoLayout = true;
1148
+ if (header.includes("vertical auto-layout")) result.layoutDirection = "VERTICAL";
1149
+ else if (header.includes("horizontal auto-layout")) result.layoutDirection = "HORIZONTAL";
1150
+ }
1151
+ return result;
1152
+ }
1153
+ function enrichNodeWithStyles(node, styles) {
1154
+ if (styles.layoutMode && !node.layoutMode) node.layoutMode = styles.layoutMode;
1155
+ if (styles.layoutPositioning && !node.layoutPositioning) node.layoutPositioning = styles.layoutPositioning;
1156
+ if (styles.layoutSizingHorizontal && !node.layoutSizingHorizontal) node.layoutSizingHorizontal = styles.layoutSizingHorizontal;
1157
+ if (styles.layoutSizingVertical && !node.layoutSizingVertical) node.layoutSizingVertical = styles.layoutSizingVertical;
1158
+ if (styles.itemSpacing !== void 0 && node.itemSpacing === void 0) node.itemSpacing = styles.itemSpacing;
1159
+ if (styles.paddingLeft !== void 0 && node.paddingLeft === void 0) node.paddingLeft = styles.paddingLeft;
1160
+ if (styles.paddingRight !== void 0 && node.paddingRight === void 0) node.paddingRight = styles.paddingRight;
1161
+ if (styles.paddingTop !== void 0 && node.paddingTop === void 0) node.paddingTop = styles.paddingTop;
1162
+ if (styles.paddingBottom !== void 0 && node.paddingBottom === void 0) node.paddingBottom = styles.paddingBottom;
1163
+ if (styles.fills && !node.fills) node.fills = styles.fills;
1164
+ if (styles.effects && !node.effects) node.effects = styles.effects;
1165
+ }
1166
+
962
1167
  // src/core/adapters/figma-mcp-adapter.ts
963
1168
  var TAG_TYPE_MAP = {
964
1169
  canvas: "CANVAS",
@@ -1089,6 +1294,54 @@ function parseMcpMetadataXml(xml, fileKey, fileName) {
1089
1294
  styles: {}
1090
1295
  };
1091
1296
  }
1297
+ function enrichWithDesignContext(file, designContextCode, targetNodeId) {
1298
+ const header = parseCodeHeader(designContextCode);
1299
+ const styles = parseDesignContextCode(designContextCode);
1300
+ if (header.hasAutoLayout === true && header.layoutDirection) {
1301
+ if (!styles.layoutMode) styles.layoutMode = header.layoutDirection;
1302
+ } else if (header.hasAutoLayout === false) {
1303
+ delete styles.layoutMode;
1304
+ }
1305
+ if (targetNodeId) {
1306
+ const node = findNodeById2(file.document, targetNodeId);
1307
+ if (node) {
1308
+ enrichNodeWithStyles(node, styles);
1309
+ enrichChildrenFromCode(node, designContextCode);
1310
+ return;
1311
+ }
1312
+ }
1313
+ enrichNodeWithStyles(file.document, styles);
1314
+ enrichChildrenFromCode(file.document, designContextCode);
1315
+ }
1316
+ function enrichChildrenFromCode(parent, code) {
1317
+ if (!parent.children) return;
1318
+ for (const child of parent.children) {
1319
+ const escapedName = child.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1320
+ const commentPattern = new RegExp(
1321
+ `\\{/\\*\\s*${escapedName}(?:\\s*\u2014[^*]*)?\\s*\\*/\\}\\s*\\n\\s*<\\w+[^>]*className="([^"]*)"`
1322
+ );
1323
+ const match = code.match(commentPattern);
1324
+ if (match?.[1]) {
1325
+ const childStyles = parseDesignContextCode(
1326
+ `<div className="${match[1]}">`
1327
+ );
1328
+ enrichNodeWithStyles(child, childStyles);
1329
+ }
1330
+ if (child.children) {
1331
+ enrichChildrenFromCode(child, code);
1332
+ }
1333
+ }
1334
+ }
1335
+ function findNodeById2(node, id) {
1336
+ if (node.id === id) return node;
1337
+ if (node.children) {
1338
+ for (const child of node.children) {
1339
+ const found = findNodeById2(child, id);
1340
+ if (found) return found;
1341
+ }
1342
+ }
1343
+ return void 0;
1344
+ }
1092
1345
 
1093
1346
  // src/core/engine/design-data-parser.ts
1094
1347
  function parseDesignData(data, fileKey, fileName) {
@@ -3127,16 +3380,21 @@ Two ways to provide design data:
3127
3380
 
3128
3381
  Typical flow with Figma MCP (recommended, no token needed):
3129
3382
  Step 1: Call the official Figma MCP's get_metadata tool to get the node tree
3130
- Step 2: Pass the result as designData to this tool
3383
+ Step 2: Call the official Figma MCP's get_design_context tool on the same node to get style data
3384
+ Step 3: Pass get_metadata result as designData and get_design_context code as designContext to this tool
3385
+
3386
+ The designContext parameter enriches analysis with style information (colors, layout, spacing, effects)
3387
+ that get_metadata alone cannot provide. Without it, token and layout rules may not fire.
3131
3388
 
3132
3389
  IMPORTANT \u2014 Before calling this tool, check which data source is available:
3133
- - If the official Figma MCP (https://mcp.figma.com/mcp) is connected: use get_metadata \u2192 designData flow. No token needed.
3390
+ - If the official Figma MCP (https://mcp.figma.com/mcp) is connected: use get_metadata + get_design_context \u2192 designData + designContext flow. No token needed.
3134
3391
  - If Figma MCP is NOT connected: use the input parameter with a Figma URL. This requires a FIGMA_TOKEN.
3135
3392
  Tell the user: "The official Figma MCP server is not connected. To use without a token, set it up:
3136
3393
  claude mcp add -s project -t http figma https://mcp.figma.com/mcp
3137
3394
  Otherwise, provide a Figma API token via FIGMA_TOKEN env var or the token parameter."`,
3138
3395
  {
3139
3396
  designData: z.string().optional().describe("Figma node data from Figma MCP get_metadata (XML or JSON). Pass this instead of input when using Figma MCP."),
3397
+ designContext: z.string().optional().describe("Code output from Figma MCP get_design_context. Enriches designData with style info (colors, layout, spacing, effects). Highly recommended alongside designData."),
3140
3398
  input: z.string().optional().describe("Figma URL. Used when designData is not provided. Requires FIGMA_TOKEN."),
3141
3399
  fileKey: z.string().optional().describe("Figma file key (used with designData to generate deep links)"),
3142
3400
  fileName: z.string().optional().describe("Figma file name (used with designData for display)"),
@@ -3152,13 +3410,16 @@ IMPORTANT \u2014 Before calling this tool, check which data source is available:
3152
3410
  openWorldHint: true,
3153
3411
  title: "Analyze Figma Design"
3154
3412
  },
3155
- async ({ designData, input, fileKey, fileName, token, preset, targetNodeId, configPath, customRulesPath }) => {
3413
+ async ({ designData, designContext, input, fileKey, fileName, token, preset, targetNodeId, configPath, customRulesPath }) => {
3156
3414
  trackEvent(EVENTS.MCP_TOOL_CALLED, { tool: "analyze" });
3157
3415
  try {
3158
3416
  let file;
3159
3417
  let nodeId;
3160
3418
  if (designData) {
3161
3419
  file = parseDesignData(designData, fileKey ?? "unknown", fileName);
3420
+ if (designContext) {
3421
+ enrichWithDesignContext(file, designContext, targetNodeId);
3422
+ }
3162
3423
  } else if (input) {
3163
3424
  const loaded = await loadFile(input, token);
3164
3425
  file = loaded.file;