第 3 章:从 Figma 导出到 TS 文件

上一章讲了在 Figma 里怎么画一个多色图标。这一章讲画完之后怎么把它变成 sag-icons 能用的数据。

核心问题:Figma 导出的 SVG 和 sag-icons 需要的"图标字符串"不是一回事。

具体差别:

Figma 导出的 SVGsag-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 里:

  1. 选中 icon-home 这个 Frame
  2. 右下角 Export panel → 选 SVG
  3. 点击 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" />

每加一个图标,需要:

  1. 在 src/data/icons/<category>/ 下新建 <name>.ts
  2. 导出 const 字符串
  3. 在 src/data/index.ts 里 import + 加进 ICON_PATHS
  4. 在 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