Koobiq Icons Figma Audit
此技能验证 Koobiq 图标 Figma 文件中的图标 组件 是否符合库的结构约定,确保 figma:sync(@figma-export 管道)能够生成干净、正确的 SVG。在 Figma 中发现问题远比在导出后发现问题成本更低,因为导出会静默地将任何找到的内容进行转换——错误的图层名称、多余的节点或偏离网格的坐标都会变成有问题的 SVG,且没有错误信息。
默认情况下,这是一个 只读审计:检查,然后报告清晰的通过/失败清单以及具体、可操作的结果。仅在用户明确要求修复时修改 Figma 文件(参见“提供修复”)。
先决条件
-
必须连接 Figma MCP。此技能通过
get_metadata(结构概览)、use_figma(通过插件 API 进行精确检查)和get_screenshot(视觉完整性检查)驱动它。 -
在每次调用
use_figma之前,请加载figma-use技能——它记录了插件 API 规则并防止常见、难以调试的失败。 -
文件键和尺寸页面是固定的(已验证与实时文档一致)。要跳过页面发现往返,可以直接按节点 ID 跳转到页面:
尺寸 页面节点 ID 16 1320:1738724 42149:86032 42149:86248 42149:86164 42149:863文件键:
WvUarBj8tO256vML4PkBLl。每个页面名称就是尺寸后缀;名为foo_24的图标位于页面24上。文档: https://www.figma.com/design/WvUarBj8tO256vML4PkBLl/%F0%9F%9F%A5-koobiq-%C2%B7-icons?node-id=1320-17387- 此文件键是唯一真理来源——它与
config.mjs中的键匹配。packages/icons/README.md中的“绘制图标”链接指向一个 不同的、错误的文件——忽略它,信任上面的键。 - 不带
nodeId的get_metadata只列出第一页(ICONS,其中不包含图标组件)——它 不会 枚举尺寸页面。不要将其用于页面发现;请使用上面的节点 ID。
- 此文件键是唯一真理来源——它与
如果无法访问文件
如果 Figma 调用失败——身份验证/权限错误、“节点不存在”、文件为空或意外、或 MCP 无响应——请停止操作并向用户展示问题,而不是猜测。告诉他们哪里失败了,并请他们执行以下一项操作:
- 提供不同的/正确的 Figma 链接(
config.mjs中的文件键可能已过时,或者他们粘贴了另一个文件或分支的链接),或 - 重新检查仓库
.env中的FIGMA_TOKEN(figma-export/ MCP 访问令牌可能丢失、过期或吊销),或 - 确认他们的 Figma 帐户有权访问该文件(这是一个私有设计文件——他们可能需要被邀请或使用关联了有权限帐户的令牌)。
不要根据假设编造审计结果;检查只有在针对实时文件时才有意义。
并行读取与页面上下文
Figma 读取调用可以并行运行,但仅限于 任务中第一次 Figma 编辑之前。任何编辑开始后,请一次只运行一个 Figma 调用。
切换页面的并行读取可能会相互干扰。在一次观察到的运行中,针对页面 24 的读取返回了页面 16 的数据(相同组件数量,但名称错误),审计报告了 24px 变体缺失。保护每次切换页面的读取:
// 在每次切换页面的读取中使用。仅返回属于 PAGE_ID 的节点的数据。
const PAGE_ID = "42149:860"; // 上表中的页面节点 ID
const NAMES = ["foo_24"]; // 要查找的组件名称
const page = await figma.getNodeByIdAsync(PAGE_ID);
await figma.setCurrentPageAsync(page);
if (figma.currentPage.id !== PAGE_ID) throw new Error("读取前页面上下文不匹配");
const pageOf = (n) => { let p = n; while (p && p.type !== "PAGE") p = p.parent; return p; };
const hits = figma.currentPage
.findAllWithCriteria({ types: ["COMPONENT", "COMPONENT_SET"] })
.filter((n) => NAMES.includes(n.name) && pageOf(n) && pageOf(n).id === PAGE_ID)
.map((n) => ({ name: n.name, id: n.id, w: n.width, h: n.height }));
if (figma.currentPage.id !== PAGE_ID) throw new Error("读取期间页面上下文已更改");
return { pageId: PAGE_ID, hits };
如果 guard 抛出异常,请丢弃该结果并重新运行读取。任何“缺失”声明(例如,“没有 24px 变体”)必须基于对该页面的受保护读取,或基于按节点 ID 的查找。永远不要从抛出异常的读取或未检查的页面中报告缺失。
步骤 1 — 解析目标
用户可以给你以下任何一种:
- 一个 Figma 节点 URL 或 ID(例如
node-id=44731-6643)→ 精确检查该节点,然后按名称查找其兄弟尺寸变体以进行跨变体检查。 - 一个图标名称(例如
floating-window或floating-window_16)→ 在图标页面中查找所有尺寸变体并全部检查。新图标通常是 未发布的,甚至可能是未命名的,因此如果名称匹配失败,请在相关页面上列出组件并让用户指向该节点,或按节点 ID 检查。
页面名称等于尺寸后缀。尺寸扫描: 检查所有五个页面(16 / 24 / 32 / 48 / 64)中的每个请求名称,因为图标可能存在于用户未提及的尺寸中。使用先决条件表中的页面节点 ID,每个页面一次 use_figma 调用,每次调用都带有“并行读取与页面上下文”中的 guard。仅在 Figma 编辑之前并行运行它们。将结果作为表格报告(名称 × 尺寸 → 节点 ID 或缺失)。
步骤 2 — 检查
运行此只读脚本(调整 targets/页面 ID)。它将返回以下规则所需的所有内容。同时获取一个 get_screenshot 以便你可以确认字形实际渲染。
// 粘贴到 use_figma (skillNames: "figma-use")。设置 PAGE_ID 和 NAMES。
// 页面节点 ID:16=1320:17387 24=42149:860 32=42149:862 48=42149:861 64=42149:863
const PAGE_ID = "1320:17387"; // 页面名称是尺寸,例如 "16"
const NAMES = ["floating-window_16"]; // 在此页面上要审计的组件名称
figma.skipInvisibleInstanceChildren = false; // false:规则 G 也必须看到隐藏图层
const page = await figma.getNodeByIdAsync(PAGE_ID);
await figma.setCurrentPageAsync(page);
if (figma.currentPage.id !== PAGE_ID) throw new Error("读取前页面上下文不匹配");
const toHex = (c) => {
const h = (n) => Math.round(n * 255).toString(16).padStart(2, "0").toUpperCase();
return "#" + h(c.r) + h(c.g) + h(c.b);
};
const describeFill = (n) => {
if (!n.fills || n.fills === figma.mixed) return n.fills === figma.mixed ? "MIXED" : null;
return n.fills.filter((f) => f.visible !== false).map((f) => ({
type: f.type,
hex: f.type === "SOLID" ? toHex(f.color) : undefined,
opacity: f.opacity,
boundVar: f.boundVariables && f.boundVariables.color ? f.boundVariables.color.id : null,
}));
};
const pageOf = (n) => { let p = n; while (p && p.type !== "PAGE") p = p.parent; return p; };
const all = page.findAllWithCriteria({ types: ["COMPONENT", "COMPONENT_SET"] })
.filter((n) => pageOf(n) && pageOf(n).id === PAGE_ID);
const report = [];
for (const name of NAMES) {
const c = all.find((x) => x.name === name);
if (!c) { report.push({ name, found: false }); continue; }
report.push({
name: c.name, id: c.id, type: c.type,
width: c.width, height: c.height, x: c.x, y: c.y,
layoutMode: c.layoutMode,
clipsContent: c.clipsContent,
description: c.description,
children: c.children.map((ch) => ({
name: ch.name, type: ch.type,
w: ch.width, h: ch.height, x: ch.x, y: ch.y,
constraints: ch.constraints,
fills: describeFill(ch),
styles: { fill: ch.fillStyleId, stroke: ch.strokeStyleId, effect: ch.effectStyleId },
boundVariableFields: ch.boundVariables ? Object.keys(ch.boundVariables) : [],
})),
});
}
if (figma.currentPage.id !== PAGE_ID) throw new Error("读取期间页面上下文已更改");
return report;
为了可靠地验证 颜色约定,还以相同方式读取一个已知正确的同尺寸兄弟图标(例如 list_16),并比较主形状的绑定变量 键(规则 D)。名义上的约定十六进制值与文件中实际存储的值不同(参见规则 D),因此与真实邻居进行比较比匹配字面十六进制值更值得信赖。
步骤 3 — 对照规则进行检查
每项报告为 ✅ 通过 / ❌ 失败,并附上观察到的值以及失败时应为的值。
A. 节点类型和名称
-
节点是一个
COMPONENT(或是尺寸变体的COMPONENT_SET),而不是FRAME、GROUP或INSTANCE。只有组件才能导出。 -
首字符——先检查此项,它会改变名称的含义:
- 以
.或_开头 → 一个 内部组件,有意不导出。 导出的.figmaexportrc.mjs中的filterComponent会丢弃任何匹配^[._]的名称。这是设计使然,不是错误——不要将其作为有问题的图标进行审计。仅当用户期望此节点导出时提出(那么修复方法是重命名,去掉前导的./_)。 - 以
-开头 → 无效,不允许。 标记为失败。 - 以 数字 开头 → 允许但 不鼓励。 它仍然会导出(例如
3-columns_32),但名称最好以英文字母开头——建议用户重命名。软性建议,不是阻塞项。 - 以 英文小写字母 开头 → 首选情况。
- 以
-
名称格式 — 可导出的图标名称必须匹配此正则表达式:
^[a-z0-9]+(-[a-z0-9]+)*_(16|24|32|48|64)$仅限英文小写字母和数字;单词由 单个 连字符连接(无
--,无前导/尾随连字符);然后 恰好一个 下划线;然后是有效的尺寸后缀。无大写字母、空格、西里尔字母/其他文字或其他标点符号。- ✅
floating-window_16、api_24、arrow-up-to-bracket_16(以及,格式有效但数字不鼓励的3-columns_32) - ❌
Floating-Window_16(大写)·плавающее-окно_16(非英文)·floating_window_16/icon__16(两个下划线)·floating--window_16(双连字符)·floating-window-16(尺寸前用连字符代替下划线)·-floating-window_16(前导连字符)·floating-window_20(尺寸不在允许集合中)·floating-window_16(尾随空格)
- ✅
-
-o后缀 = 轮廓变体。 没有-o的名称是填充变体。像cloud_24/cloud-o_24或cloud-arrow-up_24/cloud-arrow-up-o_24这样的对是一个图标的两种样式。这是有效的,不是冲突或重复;不要因此要求重命名。 -
数字后缀必须等于组件的实际尺寸(规则 B)。一个
..._16却是 24×24 的是个错误。
B. 几何和布局
- 无自动布局:
layoutMode === "NONE"。自动布局会重新排列子元素并破坏导出。 - 固定且为方形:
width === height === size。尺寸必须是整数像素(像素网格对齐);分数组件尺寸意味着图标不是在网格上绘制的。 - 关闭剪裁内容:
clipsContent === false。“剪裁内容”切换开关必须禁用。对于图标的所有尺寸变体,它必须相同(关闭)。 当它开启时,任何超出框架边界的图稿会在导出时被静默裁剪,产生一个被剪裁或截断的图标。
C. 根结构(这是大多数真实错误隐藏的地方)
- 单色图标:恰好 一个 根子元素——一个名为
shape的VECTOR。 - 双色图标:恰好 两个 根子元素——
shape(主要)和shape-2(次要)。没有其他元素。 - 组件根目录下没有其他节点:没有多余的框架、组、矩形、切片、重复的向量或隐藏的辅助图层。每个多余节点都会出现在导出的 SVG 中。
- 每个形状的 约束在两个轴上都是
SCALE(horizontal: "SCALE",vertical: "SCALE"),以便图稿随框架缩放。- 严重性:警告(卫生),不是导出阻塞项。 约束控制组件在 Figma 中的调整大小方式,而不是导出的路径数据。与其他批准的 Figma 编辑一起修复它们,因为实例和调整大小依赖于它们。
- 理想情况下,每个形状都是一个单一的扁平路径(参见仓库 README “绘制图标”步骤:轮廓、扁平化、非零填充规则)。将多个子路径或未扁平的组标记为警告。
D. 填充颜色约定
约定(仓库 README “SVG 颜色区域指南”):
- 主要区域
shape→ 黑色#21222C。导出会移除其fill,以便 SVG 继承currentColor。 - 次要区域
shape-2→ 红色#E21D03。导出会将其重写为fill="currentColor"。
实际情况:在生产文件中,主填充绑定到共享的 Icon/Contrast 变量,该变量 解析为 #000819,不透明度为 0.85 paint opacity——不是 字面量的 #21222C。所以:
- 参考键:
91eb54e0eae961c540c994192c4eed18e4ea1825— 来自🟥 koobiq · components 19(集合键c78fab428cf2003c9aa53d86b132a2fa95bacb2e2)的Icon/Contrast。 - 将主填充的绑定变量 键 与该参考值进行比较 (
(await figma.variables.getVariableByIdAsync(boundId)).key),并检查填充不透明度是否为 0.85。不要比较名称:Icon/Contrast存在于多个库和集合中。图标中唯一允许的 变量 就是这个,通过键识别。一个同名但键不同的变量(来自另一个库或集合)是一个 失败,而不是警告。修复:重新绑定到来自同一库集合的参考键变量。 - 次要的
shape-2必须是纯色字面量#E21D03(导出将其作为字面量十六进制进行匹配——.figmaexportrc.mjs中的secondaryZoneColorHex),或者通过键绑定到来自🟥 koobiq · components 19的Icon/Error(键e736157dcefc2a3e016ec2ddd41a7dc8cf8a5bc6)。对于变量情况,不检查填充不透明度。任何其他填充都会失败。 - 一个不与任何兄弟匹配的纯色字面量主填充,或一个不是
#E21D03的次要填充,都会在导出时出现问题...
...