figma-relai 0.2.0 → 0.2.1

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.
@@ -9,7 +9,7 @@ function createServer() {
9
9
  return new McpServer(
10
10
  {
11
11
  name: "Relai",
12
- version: "0.2.0"
12
+ version: "0.2.1"
13
13
  },
14
14
  {
15
15
  instructions: `
@@ -116,6 +116,7 @@ var FIGMA_COMMANDS = [
116
116
  "get_relaunch_data",
117
117
  // Styling
118
118
  "set_fill_color",
119
+ "set_fills",
119
120
  "set_stroke_color",
120
121
  "set_corner_radius",
121
122
  "set_blend_mode",
@@ -154,6 +155,9 @@ var FIGMA_COMMANDS = [
154
155
  "get_component_properties",
155
156
  "set_component_properties",
156
157
  "detach_instance",
158
+ "reset_instance",
159
+ "audit_colors",
160
+ "find_orphan_instances",
157
161
  // Variables
158
162
  "get_variable_collections",
159
163
  "get_variables",
@@ -705,6 +709,8 @@ var propertiesSchema = z3.object({
705
709
  blendMode: blendModeSchema.optional(),
706
710
  // Paint
707
711
  fillColor: colorSchema.optional(),
712
+ fills: z3.array(z3.record(z3.unknown())).optional().describe("Raw Paint[] \u2014 replaces the whole fill list; [] clears all fills"),
713
+ strokes: z3.array(z3.record(z3.unknown())).optional().describe("Raw Paint[] \u2014 replaces the whole stroke list; [] clears all strokes"),
708
714
  strokeColor: colorSchema.optional(),
709
715
  strokeWeight: z3.number().min(0).optional(),
710
716
  cornerRadius: z3.number().min(0).optional(),
@@ -780,6 +786,11 @@ function mapPropertiesToCommands(nodeId, p) {
780
786
  layoutSizingHorizontal: p.layoutSizingHorizontal,
781
787
  layoutSizingVertical: p.layoutSizingVertical
782
788
  });
789
+ if (p.fills !== void 0 || p.strokes !== void 0)
790
+ add("set_fills", {
791
+ ...p.fills !== void 0 ? { fills: p.fills } : {},
792
+ ...p.strokes !== void 0 ? { strokes: p.strokes } : {}
793
+ });
783
794
  if (p.fillColor !== void 0) add("set_fill_color", { color: p.fillColor });
784
795
  if (p.strokeColor !== void 0 || p.strokeWeight !== void 0)
785
796
  add("set_stroke_color", {
@@ -1016,7 +1027,7 @@ import { z as z6 } from "zod";
1016
1027
  function register5(server, sendCommand) {
1017
1028
  server.tool(
1018
1029
  "manage_components",
1019
- "Component workflow: list local components, create a component from a node, create_set (combine components as variants), instantiate (place an instance by componentKey \u2014 imports from the team library if needed), get_props / set_props (component properties on an instance), get_overrides / set_overrides (copy overrides from a source instance to targets), detach.",
1030
+ "Component workflow: list local components, create a component from a node, create_set (combine components as variants), instantiate (place an instance by componentKey \u2014 imports from the team library if needed), get_props / set_props (component properties on an instance), get_overrides / set_overrides (copy overrides from a source instance to targets), reset_instance (clear ALL overrides so the instance re-inherits its main component \u2014 returns property snapshots before/after so you can re-apply what mattered), detach.",
1020
1031
  {
1021
1032
  action: z6.enum([
1022
1033
  "list",
@@ -1027,9 +1038,10 @@ function register5(server, sendCommand) {
1027
1038
  "set_props",
1028
1039
  "get_overrides",
1029
1040
  "set_overrides",
1041
+ "reset_instance",
1030
1042
  "detach"
1031
1043
  ]),
1032
- nodeId: z6.string().optional().describe("Target node (create/get_props/set_props/detach)"),
1044
+ nodeId: z6.string().optional().describe("Target node (create/get_props/set_props/reset_instance/detach)"),
1033
1045
  componentIds: z6.array(z6.string()).optional().describe("create_set: components to combine"),
1034
1046
  componentKey: z6.string().optional().describe("instantiate: component key (or node id)"),
1035
1047
  x: z6.number().optional().describe("instantiate: position"),
@@ -1079,6 +1091,9 @@ function register5(server, sendCommand) {
1079
1091
  targetNodeIds: args.targetNodeIds
1080
1092
  });
1081
1093
  break;
1094
+ case "reset_instance":
1095
+ result = await sendCommand("reset_instance", { nodeId: args.nodeId });
1096
+ break;
1082
1097
  case "detach":
1083
1098
  result = await sendCommand("detach_instance", { nodeId: args.nodeId });
1084
1099
  break;
@@ -1842,7 +1857,8 @@ function register17(server, sendCommand) {
1842
1857
  // src/tools/v2/context.ts
1843
1858
  var context_exports = {};
1844
1859
  __export(context_exports, {
1845
- register: () => register18
1860
+ register: () => register18,
1861
+ styleCountsFrom: () => styleCountsFrom
1846
1862
  });
1847
1863
  import { z as z19 } from "zod";
1848
1864
 
@@ -2020,6 +2036,22 @@ function hasBinding(boundVars, prefix) {
2020
2036
  }
2021
2037
 
2022
2038
  // src/tools/v2/context.ts
2039
+ function styleCountsFrom(styles) {
2040
+ const counts = { paint: 0, text: 0, effect: 0, grid: 0 };
2041
+ if (Array.isArray(styles)) {
2042
+ for (const s of styles) {
2043
+ const t = (s.type || "").toLowerCase();
2044
+ if (t in counts) counts[t]++;
2045
+ }
2046
+ } else if (styles && typeof styles === "object") {
2047
+ const o = styles;
2048
+ counts.paint = Array.isArray(o.paintStyles) ? o.paintStyles.length : 0;
2049
+ counts.text = Array.isArray(o.textStyles) ? o.textStyles.length : 0;
2050
+ counts.effect = Array.isArray(o.effectStyles) ? o.effectStyles.length : 0;
2051
+ counts.grid = Array.isArray(o.gridStyles) ? o.gridStyles.length : 0;
2052
+ }
2053
+ return { ...counts, total: counts.paint + counts.text + counts.effect + counts.grid };
2054
+ }
2023
2055
  function register18(server, sendCommand) {
2024
2056
  server.tool(
2025
2057
  "get_document_overview",
@@ -2035,30 +2067,21 @@ function register18(server, sendCommand) {
2035
2067
  // Older plugin builds don't have the handler — degrade quietly
2036
2068
  sendCommand("get_conventions", {}).catch(() => null)
2037
2069
  ]);
2038
- const componentCount = docInfo.componentCount ?? 0;
2039
- const stylesByType = { paint: 0, text: 0, effect: 0, grid: 0 };
2040
- if (Array.isArray(styles)) {
2041
- for (const s of styles) {
2042
- const t = (s.type || "").toLowerCase();
2043
- if (t === "paint") stylesByType.paint++;
2044
- else if (t === "text") stylesByType.text++;
2045
- else if (t === "effect") stylesByType.effect++;
2046
- else if (t === "grid") stylesByType.grid++;
2047
- }
2048
- }
2070
+ const componentCount = docInfo.componentCount ?? null;
2071
+ const stylesByType = styleCountsFrom(styles);
2049
2072
  const data = {
2050
2073
  name: docInfo.name,
2051
2074
  currentPage: docInfo.currentPage,
2052
2075
  pages: docInfo.pages || [],
2053
2076
  counts: {
2054
2077
  components: componentCount,
2055
- styles: Array.isArray(styles) ? styles.length : 0,
2078
+ styles: stylesByType.total,
2056
2079
  variableCollections: Array.isArray(collections) ? collections.length : 0
2057
2080
  }
2058
2081
  };
2059
2082
  const conventionsText = conventions?.content ?? null;
2060
2083
  return standardResult({
2061
- summary: `"${data.name}" \u2014 ${data.pages.length} pages, ${data.counts.components} components, ${data.counts.styles} styles, ${data.counts.variableCollections} variable collections` + (conventionsText ? ". This file has conventions \u2014 follow them (see data.conventions)." : ""),
2084
+ summary: `"${data.name}" \u2014 ${data.pages.length} pages, ` + (data.counts.components != null ? `${data.counts.components} components, ` : "") + `${data.counts.styles} styles, ${data.counts.variableCollections} variable collections` + (data.counts.components == null ? " (component inventory via get_design_system)" : "") + (conventionsText ? ". This file has conventions \u2014 follow them (see data.conventions)." : ""),
2062
2085
  data: { ...data, ...conventionsText ? { conventions: conventionsText } : {} },
2063
2086
  recommended_next: [
2064
2087
  { tool: "get_selection_context", reason: "Inspect currently selected nodes" },
@@ -2256,13 +2279,7 @@ function register18(server, sendCommand) {
2256
2279
  modes: Array.isArray(c.modes) ? c.modes : [],
2257
2280
  variableCount: Array.isArray(c.variableIds) ? c.variableIds.length : 0
2258
2281
  }));
2259
- const stylesByType = { paint: 0, text: 0, effect: 0, grid: 0 };
2260
- if (Array.isArray(styles)) {
2261
- for (const s of styles) {
2262
- const t = (s.type || "").toLowerCase();
2263
- if (t in stylesByType) stylesByType[t]++;
2264
- }
2265
- }
2282
+ const { total: _ignored, ...stylesByType } = styleCountsFrom(styles);
2266
2283
  const totalVars = collectionSummaries.reduce((sum, c) => sum + c.variableCount, 0);
2267
2284
  const data = {
2268
2285
  collections: collectionSummaries,
@@ -2535,7 +2552,25 @@ function registerAnalysisTools(server, sendCommand) {
2535
2552
  }
2536
2553
  const unboundColors = [];
2537
2554
  const counters = { totalProps: 0, boundCount: 0 };
2555
+ let hiddenCount = 0;
2556
+ let scanned = 0;
2557
+ let capped = false;
2538
2558
  for (const id of targetIds) {
2559
+ let audit = null;
2560
+ try {
2561
+ audit = await sendCommand("audit_colors", { nodeId: id }, 6e4);
2562
+ } catch {
2563
+ audit = null;
2564
+ }
2565
+ if (audit && typeof audit.totalProperties === "number") {
2566
+ counters.totalProps += audit.totalProperties;
2567
+ counters.boundCount += audit.boundCount ?? 0;
2568
+ hiddenCount += audit.hiddenCount ?? 0;
2569
+ scanned += audit.scanned ?? 0;
2570
+ capped = capped || !!audit.capped;
2571
+ for (const issue of audit.issues ?? []) unboundColors.push(issue);
2572
+ continue;
2573
+ }
2539
2574
  const [nodeInfo, boundVars] = await Promise.all([
2540
2575
  sendCommand("get_node_info", { nodeId: id, depth: 2, maxNodes: 200 }),
2541
2576
  sendCommand("get_bound_variables", { nodeId: id }).catch(() => null)
@@ -2557,11 +2592,15 @@ function registerAnalysisTools(server, sendCommand) {
2557
2592
  boundCount: counters.boundCount,
2558
2593
  unboundCount: unboundColors.length,
2559
2594
  tokenCoverage,
2560
- unboundColors
2595
+ unboundColors,
2596
+ ...hiddenCount ? { hiddenCount } : {},
2597
+ ...scanned ? { scanned } : {},
2598
+ ...capped ? { capped } : {}
2561
2599
  };
2562
- const warnings = unboundColors.length > 0 ? [{ category: "tokens", message: `${unboundColors.length} color(s) not bound to design tokens` }] : [];
2600
+ const visibleIssues = unboundColors.length - hiddenCount;
2601
+ const warnings = unboundColors.length > 0 ? [{ category: "tokens", message: `${unboundColors.length} color(s) not bound to design tokens${hiddenCount ? ` (${hiddenCount} on hidden paints)` : ""}` }] : [];
2563
2602
  return standardResult({
2564
- summary: `Color audit: ${unboundColors.length} unbound color(s) found. Token coverage: ${Math.round(data.tokenCoverage * 100)}%`,
2603
+ summary: `Color audit: ${visibleIssues} unbound color(s)` + (hiddenCount ? ` + ${hiddenCount} on hidden paints` : "") + `. Token coverage: ${Math.round(data.tokenCoverage * 100)}%` + (capped ? " (scan capped \u2014 large subtree)" : ""),
2565
2604
  data,
2566
2605
  warnings,
2567
2606
  recommended_next: unboundColors.length > 0 ? [
@@ -3159,6 +3198,21 @@ function register20(server, sendCommand) {
3159
3198
  fix: !adequate ? { tool: "update_node", reason: "Increase size to meet touch target minimum", args: { nodeId: targetId } } : void 0
3160
3199
  });
3161
3200
  }
3201
+ try {
3202
+ const orph = await sendCommand("find_orphan_instances", { nodeId: targetId }, 6e4);
3203
+ if (orph && typeof orph.scanned === "number") {
3204
+ const found = orph.orphans ?? [];
3205
+ results.push({
3206
+ rule: "orphaned_instances",
3207
+ passed: found.length === 0,
3208
+ severity: "warning",
3209
+ message: found.length ? `${found.length} instance(s) reference deleted components (of ${orph.scanned} scanned): ${found.slice(0, 3).map((o) => o.name).join(", ")}${found.length > 3 ? "\u2026" : ""}` : `No orphaned instances (${orph.scanned} scanned)`,
3210
+ nodeId: targetId,
3211
+ fix: found.length ? { tool: "manage_components", reason: "Re-instantiate from a living component, or detach deliberately", args: { action: "list" } } : void 0
3212
+ });
3213
+ }
3214
+ } catch {
3215
+ }
3162
3216
  const passed = results.filter((r) => r.passed).length;
3163
3217
  const failed = results.filter((r) => !r.passed).length;
3164
3218
  const data = {
@@ -3459,4 +3513,4 @@ export {
3459
3513
  listToolCatalog,
3460
3514
  registerPrompts
3461
3515
  };
3462
- //# sourceMappingURL=chunk-5YB7XMT6.js.map
3516
+ //# sourceMappingURL=chunk-EBP7POOP.js.map