Novu Figma 插件技能:用 Plugin API 程序化创建与管理文本样式的完整模式
在 figma-use 技能 的参考体系中,text-style-patterns.md 是处理字体排印(typography)的权威参考:它给出了用 Figma Plugin API 通过 use_figma 工具在文件上下文中执行 JavaScript、创建文本样式(TextStyle)、批量建立字号阶梯(Type Ramp)、导入团队库样式并将样式批量应用到文本节点的完整代码模式。读完本文,你可以直接复用这些可运行脚本片段,在 Figma 文件中程序化地搭建与代码库 token 对齐的完整字体排印系统,并规避「字体未加载」「lineHeight 传裸数字」等高频报错。
文本样式(TextStyle)的模型与可写属性
Figma 中的文本样式是「具名、可复用的字体排印定义」,等价于设计 token 库中的字号阶梯(type ramp):一个 TextStyle 把字体族、字号、字重、行高、字距等排印属性打包进一个命名实体,可整体应用到文本节点上。它与设计变量(variables)是两种东西——Figma 没有「复合变量」类型,不能把整套排印塞进一个变量;但文本样式上的单个属性可以绑定变量(例如把 fontSize 绑到尺寸变量、fontFamily 绑到字符串变量),从而让样式参与 token 体系。这一点在 wwds-text-styles.md 中进一步展开。
从类型定义看,plugin-api-standalone.d.ts 中 interface TextStyle extends BaseStyleMixin 声明了完整的可写字段:
| 属性 | 类型 | 说明 |
|---|---|---|
name |
string |
用 / 分隔做分组(如 "Heading/XL"),斜杠仅是 UI 展示手段 |
fontSize |
number |
单位:像素 |
fontName |
FontName |
{ family, style }——设置前必须先 loadFontAsync 加载字体 |
letterSpacing |
LetterSpacing |
{ value: number, unit: 'PIXELS' | 'PERCENT' } |
lineHeight |
LineHeight |
{ value: number, unit: 'PIXELS' | 'PERCENT' } 或 { unit: 'AUTO' } |
textCase |
TextCase |
'ORIGINAL' | 'UPPER' | 'LOWER' | 'TITLE' | 'SMALL_CAPS' |
textDecoration |
TextDecoration |
'NONE' | 'UNDERLINE' | 'STRIKETHROUGH' |
paragraphSpacing / paragraphIndent |
number |
段落间距 / 段落缩进 |
description |
string |
继承自 BaseStyleMixin,常用来记录对应的 CSS 变量名 |
两个最容易踩坑的字段是 lineHeight 和 letterSpacing。它们的类型定义(plugin-api-standalone.d.ts 中 LetterSpacing 与 LineHeight)都要求对象而不是裸数字:
// 错误——裸数字会抛异常
style.lineHeight = 1.5;
style.letterSpacing = 0;
// 正确
style.lineHeight = { unit: "AUTO" }; // 自动行高
style.lineHeight = { value: 24, unit: "PIXELS" }; // 固定 24px
style.lineHeight = { value: 150, unit: "PERCENT" }; // 150% 行高
style.letterSpacing = { value: 0, unit: "PIXELS" }; // 无字距
style.letterSpacing = { value: -2, unit: "PIXELS" }; // 收紧字距
style.letterSpacing = { value: 5, unit: "PERCENT" }; // 百分比字距
读回 lineHeight 时务必先判断 unit——{ unit: 'AUTO' } 形式没有 value 键,直接访问会拿到 undefined。同样的规则也适用于 TextNode 的同名属性,与 gotchas.md 中 "lineHeight and letterSpacing must be objects, not bare numbers" 一节一致。
列出本地文本样式
figma.getLocalTextStylesAsync() 返回文件中所有本地文本样式(getLocalTextStyles() 同步版已废弃,且当插件清单包含 "documentAccess": "dynamic-page" 时会直接抛异常,见 plugin-api-standalone.d.ts 中的 @deprecated 标注)。文档给出的标准列表面板脚本:
/**
* 列出所有本地文本样式及其关键属性。
*
* @returns {Promise<Array<{id: string, name: string, key: string, fontSize: number, fontName: FontName, lineHeight: LineHeight, letterSpacing: LetterSpacing}>>}
*/
async function listTextStyles() {
const styles = await figma.getLocalTextStylesAsync();
return styles.map(s => ({
id: s.id,
name: s.name,
key: s.key,
fontSize: s.fontSize,
fontName: s.fontName,
lineHeight: s.lineHeight,
letterSpacing: s.letterSpacing
}));
}
// 可直接运行的完整脚本:
const results = await listTextStyles();
return results;
注意 use_figma 的执行约定:代码自动包裹在 async 上下文中,输出只能靠顶层 return(console.log() 不会回传),且 figma.notify() 会抛 "not implemented"——这些规则定义在 SKILL.md 的 Critical Rules 一节。另外文本样式名称并不唯一(两个样式可以同名),查找已知样式时应当按 id 或 key 匹配,而不是仅靠 name。
创建文本样式:字体加载是前置硬条件
fontName 的设置有一个硬前提:对应字体必须先用 figma.loadFontAsync 加载。文档给出的完整属性创建函数:
/**
* 创建一个设置好全部排印属性的文本样式。
* 调用前字体必须已经加载。
*
* @param {string} name - 斜杠分隔的名称,如 "body/base"
* @param {{ family: string, style: string }} fontName
* @param {number} fontSize - 像素
* @param {{ value: number, unit: 'PIXELS' | 'PERCENT' } | { unit: 'AUTO' }} lineHeight
* @param {{ value: number, unit: 'PIXELS' | 'PERCENT' }} [letterSpacing]
* @param {string} [description] - 例如对应 CSS 变量 "CSS: var(--font-body-base)"
* @returns {TextStyle}
*/
function createTextStyleFull(name, fontName, fontSize, lineHeight, letterSpacing, description) {
const style = figma.createTextStyle();
style.name = name;
style.fontName = fontName;
style.fontSize = fontSize;
style.lineHeight = lineHeight; // { unit: 'AUTO' } | { value, unit: 'PIXELS'|'PERCENT' }
if (letterSpacing) style.letterSpacing = letterSpacing;
if (description) style.description = description;
return style;
}
description 字段值得注意:文档推荐把它写成 CSS: var(--font-body-base) 这样的形式,把 Figma 样式和代码库里的 CSS 变量显式关联起来——这是设计 token 与前端代码保持同步的实用技巧。
发现可用字体样式:不要猜测字体名
字体样式名(style string)因字体提供商和具体 Figma 文件而异——例如 "SemiBold" 与 "Semi Bold" 是两种真实存在的写法差异。文档的要求很明确:用 figma.listAvailableFontsAsync() 发现精确的样式字符串,永远不要凭记忆猜测,也不要用 try/catch 逐个试探:
/**
* 用 listAvailableFontsAsync 发现指定字体族可用的样式名。
*
* @param {string} family - 字体族名,如 "Inter"
* @returns {Promise<string[]>} - 该字体族所有可用样式名
*/
async function getAvailableFontStyles(family) {
const allFonts = await figma.listAvailableFontsAsync();
return allFonts
.filter(f => f.fontName.family === family)
.map(f => f.fontName.style);
}
从类型定义看,listAvailableFontsAsync() 返回 Promise<Font[]>,其中 Font = { fontName: FontName }(plugin-api-standalone.d.ts)。loadFontAsync(fontName: FontName) 的结果是有缓存的——重复加载同一字体不会重新从磁盘取,但每次调用仍是一个 Promise 往返,所以不应在循环里对同一字体反复调用(类型文件 L1621-L1625 的注释明确说明了这一点)。SKILL.md 更进一步强调:只要文档里已存在文本节点,脚本开头就应预加载这些节点用到的所有字体——因为 appendChild、findAll 回调等许多操作在涉及未加载字体时同样会失败,而不仅是「改文本内容」时。
批量创建字号阶梯(Type Ramp)
这是文档中最有实战价值的多步模式:从 token 定义数组一次性创建完整字号阶梯,内部处理了字体加载、去重和幂等性(重复执行不会创建重名样式)。每条形目的格式为 [name, fontFamily, fontStyle, fontSize_px, lineHeight, cssVar]:
/**
* 从 token 定义数组创建完整字号阶梯。
* 处理字体加载、去重与幂等性。
*
* 每条目: [name, fontFamily, fontStyle, fontSize_px, lineHeight, cssVar]
* - lineHeight: { unit: 'AUTO' } 或 { value: number, unit: 'PIXELS' | 'PERCENT' }
*
* @param {Array} defs - [name, fontFamily, fontStyle, fontSize, lineHeight, cssVar] 元组数组
* @returns {Promise<{ created: string[], skipped: string[] }>}
*/
async function createTypeRamp(defs) {
const uniqueFonts = new Set();
for (const [, family, style] of defs) {
uniqueFonts.add(JSON.stringify({ family, style }));
}
await Promise.all(
[...uniqueFonts].map(f => figma.loadFontAsync(JSON.parse(f)))
);
const existing = new Set(
(await figma.getLocalTextStylesAsync()).map(s => s.name)
);
const created = [];
const skipped = [];
for (const [name, family, style, fontSize, lineHeight, cssVar] of defs) {
if (existing.has(name)) {
skipped.push(name);
continue;
}
const ts = figma.createTextStyle();
ts.name = name;
ts.fontName = { family, style };
ts.fontSize = fontSize;
ts.lineHeight = lineHeight ?? { unit: 'AUTO' };
if (cssVar) ts.description = `CSS: var(${cssVar})`;
created.push(name);
}
return { created, skipped };
}
配套的可运行脚本,展示了一个覆盖标题、正文、代码字体的典型阶梯:
const defs = [
['heading/xl', 'Inter', 'Bold', 48, { unit: 'PIXELS', value: 56 }, '--font-heading-xl'],
['heading/lg', 'Inter', 'Bold', 36, { unit: 'PIXELS', value: 44 }, '--font-heading-lg'],
['body/base', 'Inter', 'Regular', 16, { unit: 'AUTO' }, '--font-body-base'],
['body/sm', 'Inter', 'Regular', 14, { unit: 'AUTO' }, '--font-body-sm'],
['code/base', 'Roboto Mono', 'Regular', 14, { unit: 'AUTO' }, '--font-code-base'],
];
const result = await createTypeRamp(defs);
return result;
值得拆解的三个工程细节:
- 字体去重加载:
uniqueFonts用JSON.stringify({ family, style })做集合键,Promise.all并发加载。阶梯里heading/xl和heading/lg共用同一字体,只加载一次; - 幂等性:创建前先
getLocalTextStylesAsync()拉取现有样式名集合,已存在的名字进入skipped直接跳过——脚本可以安全地重复执行; - 返回值可追踪:
return { created, skipped }符合技能要求「每个创建/变更节点的脚本必须return受影响对象」的规则(gotchas.md 的 "MUST return ALL created/mutated node IDs" 一节),后续调用可据此校验或清理。
导入团队库文本样式
如果样式定义在团队库(team library)而非本地,正确做法不是重新创建,而是用 figma.importStyleByKeyAsync(key) 按 key 导入:
// 按 key 导入库文本样式
const headingStyle = await figma.importStyleByKeyAsync("TEXT_STYLE_KEY");
// 应用到文本节点
await textNode.setTextStyleIdAsync(headingStyle.id);
key 是样式在文档中的唯一标识(类型定义 L11015 附近注明:节点可通过 setTextStyleIdAsync 等将样式 id 赋给自己以使属性与样式同步)。获取可用 key 的途径是 search_design_system 工具并带 includeStyles: true——它会返回可导入的样式 key。文档的取舍原则是:优先导入库样式,而不是新建本地样式,这保证了与团队设计系统的单一事实来源一致。
将文本样式批量应用到节点
创建样式本身对任何节点都没有影响,必须显式把样式 id 赋给文本节点。文档提供了一个按名称模式匹配、批量应用到当前页面全部 TEXT 节点的实用函数:
/**
* 将文本样式应用到当前页面上名称匹配给定模式的所有 TEXT 节点。
*
* @param {string} styleId - 某个 TextStyle 的 ID
* @param {string} nodeNamePattern - 对节点名称做子串匹配
* @returns {Promise<number>} - 应用了样式的节点数
*/
async function applyTextStyleToMatchingNodes(styleId, nodeNamePattern) {
const textNodes = figma.currentPage.findAllWithCriteria({ types: ['TEXT'] });
let applied = 0;
for (const node of textNodes) {
if (node.name.includes(nodeNamePattern)) {
await node.setTextStyleIdAsync(styleId);
applied++;
}
}
return applied;
}
// 可运行的完整脚本:
const applied = await applyTextStyleToMatchingNodes('STYLE_ID', 'Heading');
return { applied };
两个实现层面的补充:
findAllWithCriteria({ types: ['TEXT'] })是类型化的高效查找 API——它的返回值会被收窄为TextNode[],且在大型文档中可比findAll快数百倍(plugin-api-standalone.d.ts 的注释);- 应用样式不需要先加载字体——只有直接编辑文本内容或字体属性才需要。类型定义中
textStyleId的属性注释也指出:在dynamic-page文档访问模式下该属性只读,必须用异步的setTextStyleIdAsync(styleId)更新(plugin-api-standalone.d.ts)。
文本样式与变量的绑定
wwds-text-styles.md 补充了 token 化场景的关键 API:以下字段可通过 style.setBoundVariable(field, variable) 绑定变量——fontFamily、fontSize、fontStyle、fontWeight、letterSpacing、lineHeight、paragraphSpacing、paragraphIndent;解绑则传 null。文档的建议是只要变量存在,就用绑定代替裸值:
const ts = figma.createTextStyle();
ts.fontSize = 24; // 直接赋值,未绑定变量
const ts2 = figma.createTextStyle();
ts2.setBoundVariable("fontSize", fontSizeVariable); // 变量存在时优先用这个
类型定义中 TextStyle.boundVariables 字段(plugin-api-standalone.d.ts)和 setBoundVariable(field, variable | null) 方法(L11121)印证了这套绑定模型。
高频错误速查
综合文档与技能中的 gotchas,文本样式相关的检查清单是:
| 症状 / 错误 | 原因 | 修复 |
|---|---|---|
设置 fontName 报错 |
字体未加载 | 先 await figma.loadFontAsync({ family, style }) |
"SemiBold" vs "Semi Bold" 类失败 |
样式名因提供商/文件而异 | 用 listAvailableFontsAsync() 发现精确字符串,勿猜测 |
设置 lineHeight / letterSpacing 抛错 |
传了裸数字 | 一律用 { value, unit } 或 { unit: 'AUTO' } |
| 找不到已知样式 | 按 name 匹配 |
名称不唯一,改按 id / key 匹配 |
| 样式建了但节点没变化 | 样式不会自动生效 | 必须把样式 id 赋给节点(setTextStyleIdAsync) |
getLocalTextStyles() 抛异常 |
同步版已废弃 | 改用 getLocalTextStylesAsync() |
其中「创建样式对节点零影响」「斜杠分组只是 UI 展示」("Heading/XL" 与 "HeadingXL" 是两个不同名字)这两条认知最容易与直觉冲突,调试时值得先排除。
参考资料与延伸阅读
- 本文主体:text-style-patterns.md——列出、创建、发现字体、Type Ramp、导入库样式、应用到节点的完整代码模式
- 概念与陷阱:wwds-text-styles.md——TextStyle 模型、变量绑定、常见 gotchas
- 执行环境规则:SKILL.md——
use_figma的原子性、return输出通道、增量工作流与错误恢复 - 全部已知陷阱(WRONG/CORRECT 对照):gotchas.md
- API 面与精确签名:plugin-api-standalone.index.md(按符号检索)与 plugin-api-standalone.d.ts(完整类型文件,建议 grep 而非整读)
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00