第 3 章:从 Figma 导出到 TS 文件
上一章讲了在 Figma 里怎么画一个多色图标。这一章讲画完之后怎么把它变成 sag-icons 能用的数据。
核心问题:Figma 导出的 SVG 和 sag-icons 需要的"图标字符串"不是一回事。
具体差别:
| Figma 导出的 SVG | sag-icons 需要的字符串 |
|---|
完整的 <svg> 标签 | 只有 inner HTML(path / rect / circle) |
| 属性在 Figma 命名空间 | 需要 data-tone 属性 |
<defs>、<g> 等冗余元素 | 全部去掉 |
颜色写死成 #3b82f6 | 改成 currentColor,让 CSS 主题接管 |
这一章带你走一遍"清理 → 标 data-tone → 写 TS → 注册"完整流程。
3.1 目录约定
sag-icons 把所有图标放在 src/data/icons/<category>/<name>.ts:
src/data/icons/
nav/
workbench.ts
brand.ts
home.ts # 这个是 base 类别下的
base/
home.ts
grid.ts
book.ts
ui/
user.ts
...
每个文件就是一个图标的 TS 模块。文件名 = 图标名(kebab-case)。base/home.ts 对应 <sag-icon name="home" />。
对应目录:src/data/icons/
为什么按目录分?
- 代码可读性:打开
src/data/icons/nav/ 你知道这一类是导航图标
- 按需导入:
@jujiuyey/sag-icons/data/icons/nav/workbench(第 7 章会细讲)
- IDE 友好:文件树就是目录树
3.2 从 Figma 导出一个图标
步骤 1:选中 Frame,导出 SVG
在 Figma 里:
- 选中
icon-home 这个 Frame
- 右下角 Export panel → 选 SVG
- 点击
Export icon-home
Figma 默认会把 SVG 嵌到完整 <svg> 标签里,并且带很多 Figma 命名空间的属性:
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 9V21H19.5V9L12 3 4.5 9Z" fill="#3B82F6" stroke="#3B82F6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 14.5V21H14.5V14.5H9.5Z" fill="#FFFFFF" stroke="#FFFFFF" stroke-width="2" stroke-linejoin="round"/>
<path d="M4.5 21H19.5" stroke="#3B82F6" stroke-width="2" stroke-linecap="round"/>
</svg>
步骤 2:清理 SVG
sag-icons 不需要外层 <svg> 标签——<sag-icon> 渲染时会自己创建一个 SVG 容器。所以你只需要 innerHTML 部分:
<!-- 只要这部分 -->
<path d="M4.5 9V21H19.5V9L12 3 4.5 9Z" fill="#3B82F6" stroke="#3B82F6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 14.5V21H14.5V14.5H9.5Z" fill="#FFFFFF" stroke="#FFFFFF" stroke-width="2" stroke-linejoin="round"/>
<path d="M4.5 21H19.5" stroke="#3B82F6" stroke-width="2" stroke-linecap="round"/>
步骤 3:颜色改成 currentColor
把语义色的 fill="#3B82F6" 替换成 fill="currentColor":
- <path d="M4.5 9V21H19.5V9L12 3 4.5 9Z" fill="#3B82F6" stroke="#3B82F6" ... />
+ <path d="M4.5 9V21H19.5V9L12 3 4.5 9Z" fill="currentColor" stroke="currentColor" ... />
为什么用 currentColor?因为:
<sag-icon> 默认从 color 拿值(CSS 变量 --sag-icon-default)
- 激活态(
is-active)CSS 会把 color 切成具体色(蓝/绿/琥珀)
- 这样整个图标只用一个颜色属性就能切换主题
对应 CSS:src/css/svg-icon.ts
步骤 4:标 data-tone
每个语义色块都要加 data-tone="<color>":
<path data-tone="blue" d="..." fill="currentColor" stroke="currentColor" ... />
<path d="..." fill="#FFFFFF" stroke="#FFFFFF" ... /> <!-- 这块不标 -->
<path data-tone="blue" d="..." stroke="currentColor" ... />
这是 sag-icons 的关键设计——data-tone 属性就是 CSS 选择器的钩子:
sag-icon.is-active [data-tone="blue"] { color: var(--sag-icon-blue); }
sag-icon.is-active [data-tone="green"] { color: var(--sag-icon-green); }
注意:只有语义色块需要 data-tone。功能性元素(白门、负空间、纯装饰线)保持原样,不标。
3.3 一个真实的例子:home.ts
完整流程后,base/home.ts 长这样:
src/data/icons/base/home.ts
export const home = `<path data-tone="blue" d="M4.5 9V21H19.5V9L12 3 4.5 9Z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M9.5 14.5V21H14.5V14.5H9.5Z" fill="#fff" stroke="#fff" stroke-width="2" stroke-linejoin="round" />
<path data-tone="blue" d="M4.5 21H19.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" />`;
仔细看:
- 第一个 path 是屋顶 + 墙,标
data-tone="blue",fill="currentColor"——跟着主题
- 第二个 path 是门,
fill="#fff",不标 data-tone——永远白
- 第三个 path 是地面线,
fill="none",stroke="currentColor",标 data-tone="blue"——跟着主题
对应文件:src/data/icons/base/home.ts
3.4 另一个例子:workbench.ts
workbench 是 sag-icons 最具代表性的多色图标——四个色块:
src/data/icons/nav/workbench.ts
export const workbench = `<rect data-tone="blue" fill="currentColor" height="7" rx="2" width="7" x="3" y="4" />
<rect data-tone="cyan" fill="currentColor" height="7" rx="2" width="7" x="14" y="4" />
<rect data-tone="green" fill="currentColor" height="7" rx="2" width="7" x="3" y="15" />
<path data-tone="amber" d="M16.4 15h2.2c1.33 0 2.4 1.07 2.4 2.4v2.2c0 1.33-1.07 2.4-2.4 2.4h-2.2A2.4 2.4 0 0 1 14 19.6v-2.2c0-1.33 1.07-2.4 2.4-2.4Z" fill="currentColor" />`;
四个语义色块用了全部 4 个 data-tone(蓝/青/绿/琥珀)——这就是多色图标的核心。
注意所有元素都 fill="currentColor":CSS 主题系统给什么色,图标就是什么色。
3.5 什么时候用fill="#fff"
有些元素你不希望跟着主题切换。最常见的是"负空间"或"功能性白色"。
sag-icons 的 home 图标里那扇门就是这种——门永远白,无论图标是蓝主题还是紫主题,门都该是白的(不然会变成红门、蓝门,非常诡异)。
<path d="M9.5 14.5V21H14.5V14.5H9.5Z" fill="#fff" stroke="#fff" ... />
但 sag-icons 的 CSS 有一个细节:它给了 sag-icon 一个默认的 color,而不是直接给 fill 默认值。所以写死 fill="#fff" 的元素不会被默认色覆盖。
对应 CSS:src/css/svg-icon.ts
3.6 进阶:跨色块的复合元素
看一个更复杂的图标——sparkline-user,它有 5 个元素:
src/data/icons/nav/sparkline-user.ts
export const sparklineUser = `<circle cx="8.5" cy="8" data-tone="blue" fill="currentColor" r="3.5" />
<path data-tone="cyan" d="M3.8 18.2c.7-2.65 2.43-4.2 4.7-4.2 2.27 0 4 1.55 4.7 4.2.22.82-.42 1.6-1.28 1.6H5.08c-.86 0-1.5-.78-1.28-1.6Z" fill="currentColor" />
<path data-tone="amber" d="M15 16.5 17.2 14l1.55 1.5L21 12.2" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" />
<circle cx="17.2" cy="14" data-tone="green" fill="currentColor" r="1.1" />
<circle cx="21" cy="12.2" data-tone="green" fill="currentColor" r="1.1" />`;
5 个元素用了 4 个 data-tone(蓝、青、琥珀、绿)——所有语义色都用上了。这种图标做出来视觉很丰富,但要注意:
- 不要超过 5 个 data-tone 块(色板只有 5 色)
- 同色尽量相邻,避免视觉跳变(amber 的"折线"夹在 cyan 的人体和 green 的圆点之间,过渡自然)
- 不要给纯装饰元素标 data-tone(比如这个折线是装饰性的,但它和右边的绿点语义相关,所以标了 amber)
3.7 数据层:ICON_PATHS 注册
光有 TS 文件还不够。需要让 <sag-icon name="home" /> 找到 home 的字符串。这靠 src/data/index.ts 集中注册:
src/data/index.ts (节选)
import { brand } from "./icons/nav/brand";
import { home } from "./icons/base/home";
import { workbench } from "./icons/nav/workbench";
export const ICON_PATHS = {
brand,
home,
workbench,
// ... 全部 78 个
} as const;
export type IconName = keyof typeof ICON_PATHS;
这是 sag-icons 的图标索引。as const 是关键——它让 TS 推出字面量类型而不是 string,从而:
// ✅ TS 知道这是合法名字
<sag-icon name="workbench" />
// ❌ TS 报错
<sag-icon name="workbench-typo" />
每加一个图标,需要:
- 在
src/data/icons/<category>/ 下新建 <name>.ts
- 导出 const 字符串
- 在
src/data/index.ts 里 import + 加进 ICON_PATHS
- 在
src/data/categories.ts 里加到对应分类
对应文件:src/data/index.ts、src/data/categories.ts
3.8 手工 vs 脚本化
sag-icons 现在 78 个图标,每次新增手工改两个文件是可行的。但到 200+ 时就需要脚本。
半自动流程建议:
写一个 scripts/add-icon.mjs,输入新图标名:
// 伪代码
const name = process.argv[2];
const category = process.argv[3] || "base";
// 1. 写一个空的 .ts 文件
fs.writeFileSync(`src/data/icons/${category}/${name}.ts`,
`export const ${kebabToCamel(name)} = \`<!-- TODO: paste SVG here -->\`;\n`);
// 2. 在 index.ts 里加 import 和 entry
appendImport(`src/data/index.ts`, name, category);
appendEntry(`src/data/index.ts`, name, category);
// 3. 在 categories.ts 里加 entry
appendCategory(`src/data/categories.ts`, name, category);
然后你只需要在新建的 .ts 文件里把 SVG 字符串贴进去就行。
这不是教程的重点,但你做 200+ 图标时会需要。
3.9 这一章的产出
到这里你应该能:
- 从 Figma 导出一个 SVG 并清理成 innerHTML
- 改色为
currentColor 并加 data-tone 属性
- 写一个图标 TS 文件
- 在
ICON_PATHS 和 categories 里注册
下一章我们离开图标本身,进入包工程——怎么把这一堆文件变成可发布的 npm 包。
对应文件
| 内容 | 路径 |
|---|
| 图标数据 | src/data/icons/<category>/<name>.ts |
| 索引 | src/data/index.ts |
| 类别 | src/data/categories.ts |
| CSS 主题 | src/css/svg-icon.ts |