Skip to main content

add-icon

往本项目 `src/client/bui/icons/` 的图形注册表加图标,或替换某个语义位现有的图形。图标来源是 central icon system(centralicons.com)。两种输入都管:用户直接粘 SVG(主路径),或者只描述语义(技能按语义给候选名让用户去导)。只要用户提到图标、icon、换图标、加图标、图标太细/太粗/看不清/不好看、central、centralicons,或者点名某个具体图形(sparkle、brain、magnifying-glass 之类),就用这个技能——即使他们没说「注册表」或「bui/icons」,也即使听起来只是改一行 path。它管住五件会静默出错的事:fill 版 SVG 喂进去会渲染成「轮廓的轮廓」、非 24 坐标系漏写 viewBox 会让图形爆出画布或缩成一点、导错 weight 档会拿到为别的线宽重绘过的 path、一个语义位常有多个消费点(漏一处就是同一概念两种图形)、以及新图标的视觉线宽与同列图标不齐而代码看起来完全正常。

Jump to install

Source facts

Repository
jeasonstudio/dsh-beautiful-ui
Last source activity
August 31, 2026 at 08:59
Detected SKILL.md language
Chinese
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
4 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
add-icon
description
往本项目 `src/client/bui/icons/` 的图形注册表加图标,或替换某个语义位现有的图形。图标来源是 central icon system(centralicons.com)。两种输入都管:用户直接粘 SVG(主路径),或者只描述语义(技能按语义给候选名让用户去导)。只要用户提到图标、icon、换图标、加图标、图标太细/太粗/看不清/不好看、central、centralicons,或者点名某个具体图形(sparkle、brain、magnifying-glass 之类),就用这个技能——即使他们没说「注册表」或「bui/icons」,也即使听起来只是改一行 path。它管住五件会静默出错的事:fill 版 SVG 喂进去会渲染成「轮廓的轮廓」、非 24 坐标系漏写 viewBox 会让图形爆出画布或缩成一点、导错 weight 档会拿到为别的线宽重绘过的 path、一个语义位常有多个消费点(漏一处就是同一概念两种图形)、以及新图标的视觉线宽与同列图标不齐而代码看起来完全正常。
# 往 `bui/icons/` 加图标 图形的唯一真源是 `src/client/bui/icons/glyphs.tsx`,渲染入口是同目录的 `Icon.tsx`。 **动手前先读那两个文件的头部 docblock**——它们写着坐标系陷阱、默认值取 central 的理由、 以及为什么 key 用语义名。这份技能是流程和陷阱,那两个文件是约定本身。 ## ⚠️ 先读这条:central 是付费图标集,许可与本仓库冲突 图标来源是 **central icon system**(centralicons.com,iconists 出品)。它**不是**开源图标库, 这件事决定了下面整个获取流程的形状,也决定了几条「看起来能自动化但不要做」的事。 `iconists.co/license` 与 npm 包内 `LICENSE.md` 的 Forbidden Uses 有两条字面命中本仓库: - **`Do not share the Item or its parts publicly on the web`**——本仓库是公开的 GitHub 仓库, `glyphs.tsx` 里的 path 就是「its parts publicly on the web」。 - **`Extraction: End users cannot extract the Item for separate use`**——path 是明文源码, 谁都能复制走。 **这个冲突已经在 2026-08-28 由人裁决「知情保留」**,账记在 `bui/README.md` 的偏离表里。 所以:**不要再把这件事当成新发现重新提出来阻塞流程**,也不要「顺手」把图标换成开源库。 但也**不要扩大暴露面**——加图标时只加需要的那一档,不要囤积、不要建一个「备选池」, 不要把候选的 path 留在注释或测试 fixture 里。 这条纪律防的是「在仓库里攒出一份可复用的图标库副本」。所以它**不禁止**为技术判据留**片段化的 对比**(本文件与 `glyphs.tsx` 里各有几处 `M4.75 12.7768…` 那样的截断片段,用来证明「各 weight 的 path 是重绘的」)——那是判据的唯一证据,删掉它下一个人就会去改 `strokeWidth` 凑数。 边界是:**片段、服务于一个具体判据、不构成可用的图形**。整条可渲染的 path 只应出现在 `glyphs.tsx` 里我们真正在用的那 13 档。 许可里有一条数量上限是**当前不构成问题**的,记在这里免得下一个人重新查: `Icon Limits: 不超过 300 icons per style`。眼下 13 档,离上限很远。 ### 三条获取途径,只有第一条该走 | 途径 | 能不能用 | 说明 | | --- | --- | --- | | **web app 逐个 `Copy as SVG`** | ✅ **主路径** | 免费额度 **20 个**导出,覆盖眼下 13 档。不需要账号付费。 | | npm `@central-icons-react/<variant>` | ⚠️ 需付费 key | 包内 `license-check.js` 读 `CENTRAL_LICENSE_KEY`,缺了直接 throw,并向 `centralicons.com/license/check` 验证。且它是 **React 组件**不是裸 SVG(36MB / 2088 个组件),为 13 个图标装它不划算,仍要自己抠 path。 | | 抓 `centralicons.com/icons-data/*.bin` | ❌ **不要做** | 那是**加密**数据(412KB 高熵二进制、无 magic bytes、非 gzip/zstd/brotli),是刻意的付费保护。写解密脚本等于绕过技术保护措施。**试过了,不要再试。** | ⚠️ **不要去 fetch centralicons.com 首页想找图标数据**——它是 Next.js SPA,2.49MB 里那 31 个 内联 svg 是**网站自己 UI 用的** central 图标(顺带确认了规格),不是图标库数据。 ## central 的规格(写进注册表前必须对上) 官网口径 + 实测一致: - **坐标系 `0 0 24 24`**,2px padding,**20×20 live area**(坐标基本落在 2–22) - **`stroke-linecap: round` + `stroke-linejoin: round`**(`Icon.tsx` 无条件给,不用自己写) - **三档 weight**:`stroke-width` = **1 / 1.5 / 2** - 5 种 corner style、line/solid 两种填充 → 官网说的「30 variants」 ### web app 的 UI 标签与包名/文件名的对应 两套命名不同,挑 variant 时容易对错: | web app UI | 包名 / `icons-data` 文件名片段 | | --- | --- | | Style `line` / `solid` | `outlined` / `filled` | | Stroke `1px` / `1.5px` / `2px` | `stroke-1` / `stroke-1.5` / `stroke-2` | | Corner `0px sharp` | `square-…-radius-0` | | Corner `0px round` / `1px small` / `2px medium` / `3px large` | `round-…-radius-0` / `-1` / `-2` / `-3` | **本项目钉在 `round-outlined-radius-2-stroke-2`**,即 web app 里 **Style = line、Stroke = 2px、Corner = medium**。 ⚠️ **`Style` 要选 `line` 而不是 `solid`。** 这与下面「fill 版会毁掉整件事」是同一件事的两个 说法——`solid` 导出的是 fill 版。 ### 为什么钉 `stroke-2` 而不是默认的 `1.5` **这是实测判据不是审美偏好**,而且它复用了项目已有的一次实测结论: 视觉线宽 = `strokeWidth ÷ viewBox宽 × 渲染尺寸`。本项目槽位尺寸有 9/10/11/13/14/15px 六种, 最小的 9px 槽是 `ContextCards` 的外链。 | weight | 比值 | 9px 槽实际线宽 | | --- | --- | --- | | central `1` | 1/24 = 0.0417 | 0.375px | | central `1.5`(web app 默认) | 1.5/24 = 0.0625 | **0.5625px** | | **central `2`** | 2/24 = 0.0833 | **0.75px** | `glyphs.tsx` 头部记着:Phosphor regular 的 **0.56px 在 9px 槽「暗色下抗锯齿明显发虚」因而被 否掉**。central 的 1.5 档换算出来是 0.5625px——**选它等于重新踩回那个已经实测否过的坑**。 2 档的 0.75px 有余量。 ⚠️ **不要为了迁就某一个槽位而混 weight。** 同一张表里混两个 weight 正是 2026-08-27 那次整套 收编要治的病(当时全站 7 种 `strokeWidth`、视觉线宽跨度 129%)。要调就整表调。 ### ⚠️ weight 必须**导对**,不能靠改 `strokeWidth` 换算 **central 各 weight 的 path 是重绘的,不是同一份 path 配不同 `stroke-width`。** 端点会内缩, 以便在更粗的描边下仍然守住 20×20 live area 的边界。2026-08-28 实测: | 图标 | `stroke-1.5` 档 | `stroke-2` 档 | | --- | --- | --- | | `checkmark-1` | `M4.75 12.7768L10 19.25L19.25 4.75` | `M5 12.75L10 19L19 5` | | `list-bullets` | `M8.75 6L20.25 6` … | `M10 6L20 6` … | | `chevron-bottom` | `M20 9L13.4142…L4 9` | **完全相同** | 注意第三行:**有些图标两档一模一样**(端点不在 live area 边界上的那些)。所以「我对比了一个 图标,发现两档相同,那 weight 无所谓」是**错的推论**——你恰好挑中了不受影响的那个。 判据:**在 web app 里先把 Stroke 切到 2px,再逐个复制**。不要导出 1.5 档然后在 `glyphs.tsx` 里写 `strokeWidth: 2`——那会拿到为 1.5px 描边算过的端点位置。 ## 先分清这是哪一类活 判据是「注册表里有没有一个 key 已经在表达这件事」: - **换现有语义位的形状**(多数情况):改那一条的 `content`,key 不动。比如「思考过程的图标 换成 sparkle」——`think` 这个 key 一直在,换的只是它长什么样。 - **加新语义位**:往表里加 key。这件事比换形状重一档,因为它意味着「流里多了一类行」。 加之前先回答:**这一类行凭什么与别的行不同?** 注册表有一条被测试守着的判据——图标必须 携带信息(`ToolChips.test.tsx` 的「八档图标互不相同」)。如果新 key 只是同一类行的另一种 说法,那它不该存在;如果它确实是新的一类,那 `ToolRowIcon` 之类的消费方类型也要一起动。 不确定属于哪一类就问用户,别自己扩表。 ## 第 1 步:拿到 **stroke 版** SVG ⚠️ 这一步唯一会毁掉整件事的错误是拿到 **fill 版**。`Icon.tsx` 强制 `fill="none"` 并用 `stroke` 渲染,喂 fill 版进去画出来的是「轮廓的轮廓」——两条平行细线勾着图形的边,而不是 一个图标。它不报错,只是难看,而且很容易被当成「这个图标本来就长这样」。 判据:SVG 里有 `stroke-width` 就是 stroke 版;只有 `fill="currentColor"`(或 `fill-rule`/`clip-rule`)加一个 path 的是 fill 版。在 central 这边对应 **Style = line(对) vs solid(错)**。 ### 输入形态 A:用户直接粘了 SVG(主路径) 直接用,但**先过校验脚本**——它一次把 fill/stroke、坐标系、weight、live area 全查掉, 并输出可直接贴进注册表的 `content`: ```bash python3 .claude/skills/add-icon/scripts/central.py < /tmp/icon.svg # 或者直接管道:pbpaste | python3 .claude/skills/add-icon/scripts/central.py ``` 纯本地、无网络。它做四件事:剥掉由 `Icon` 写到 svg 根上的描边属性(`stroke` / `stroke-width` / `stroke-linecap` / `stroke-linejoin`)与 `<rect fill="none">` 占位、 判 fill 版、报比值与六个槽位的视觉线宽、查坐标是否落在 20×20 live area 内。 ### 输入形态 B:用户只给了语义 **不能自己去搜**——图标数据取不到(见上面那张三途径表)。能做的是**给候选名**,让用户去 web app 导: `references/central-slots.md` 里有眼下 13 个语义位各自的候选名与选定理由。要找表外的新语义 位时,central 的命名**同时收语义词与符号词**(`aria-label` 形如 `pencil, edit, write`),所以按用途搜也能命中——但你查不到它,得让用户在 web app 的 `⌘K` 里搜。 ⚠️ **挑好之后不要直接落地。** 图标选择是审美与语义判断,不是技术判断:`brain-1` 和 `sparkle` 都能表达「推理」,但一个像医学示意图、一个像 AI 味的装饰,选哪个是产品口味。取 2–3 个候选、 说清各自的语义联想,让用户定。 ## 第 2 步:找出这个语义位的**全部**消费点 ⚠️ **一个语义位常有多个消费点。** 2026-08-27 换 `think` 那次,同一份图形同时活在 `bui/ToolChips.tsx` 的行图元位(工具名含 think/reason/plan 的兜底档)和 `bui/ThinkingState.tsx` 的折叠头里。只改一处的后果是同一个概念在紧邻交替出现的两种行里 长着两个样子——读起来像渲染 bug。 搬进注册表之后这件事**大部分**自动解决了(多个消费点读同一条),但仍然要确认。列出**全部** 消费点的 `name`——**两条都要跑**,因为 JSX 有单行与多行两种写法: ```bash # 单行写法:<Icon name="check" /> grep -rn '<Icon[^>]*name=' src/client --include='*.tsx' | grep -v '\.test\.' | grep -vE ':[[:space:]]*\*' # 多行写法:<Icon 换行 name={icon} grep -rn -A2 '<Icon$' src/client --include='*.tsx' | grep -E 'name=' ``` 2026-08-28 复核,两条合起来是 **12 行**,分布在 8 个组件:`CodeBlock` 2(`code` + 两态的 `check`/`copy`)、`ContextCards` 2(`context` + `external-link`)、`MessageActions` 1(`glyph` 变量 → `check`/`copy`/`fork`)、`RunHeader` 1、`SearchResult` 1、`ThinkingState` 2、`TodoRows` 1、 `ToolChips` 2。 ⚠️ **三处都是踩出来的,别简化**: 1. **只跑多行那条会漏掉一半以上。** `<Icon$` 锚在行尾,只匹配多行 JSX;而 `TodoRows` / `ContextCards` / `CodeBlock` / `SearchResult` / `MessageActions` 全是单行写法。写这份技能时 只写了多行那条,实测输出 5 行而真实是 12——差的那 7 行正是 2026-08-27 新收编的装饰图标。 2. **单行那条必须 `grep -vE ':[[:space:]]*\*'` 排掉注释**:`glyphs.tsx` / `Icon.tsx` / `ToolChips.tsx` 的 docblock 里都写着 `` `<Icon name="…" />` `` 这样的散文,会被命中。 3. **不要用 `grep 'name="<语义名>"'` 代替它们。** 那条会漏掉动态消费点:`ToolChips` 写的是 `name={icon}`、`MessageActions` 写的是 `name={glyph}`、`CodeBlock` 写的是 `name={copied ? 'check' : 'copy'}`。搜 `name="think"` **只命中 `ThinkingState.tsx`**,而 `ToolChips` 完全不出现——恰恰是这两处曾经各写一份 `think` 图形。也不要写成 `grep '<Icon' | grep 'name={'`:多行写法里 `<Icon` 和它的 `name` 不在同一行,单行管道匹配 不到任何真实代码,只会命中那些散文——那条 grep 看起来跑通了、输出了两行,其实全是注释。 看到 `name={变量}` 就往上追那个变量的取值来源(`ToolChips` 是 `ToolRowIcon` 类型 + `lib/tools.ts` 的 `KIND_ICONS` / `ICON_PATTERNS`;`MessageActions` 是 `IconButton` 的 prop), 确认这个语义名在不在里面。 如果发现某个消费点还在内联 svg 而不是走 `<Icon>`,那就是漏搬的一处,顺手搬它。查残留: ```bash grep -rn "<svg" src/client --include="*.tsx" | grep -v "\.test\." | grep -v "icons/" ``` 这条应该**只输出一行**:`TodoRows.tsx` 那个进度环。它刻意不搬——判据是「这个 svg 是一个固定 形状,还是由数据算出来的」,那个环带 `strokeDasharray` 与旋转动画,是状态指示器而不是图标。 多出别的行就是有人又内联了一个。 ## 第 3 步:写进注册表 `glyphs.tsx` 的 `REGISTRY` 里加一条或改一条 `content`。规则: | 情形 | 怎么写 | | --- | --- | | central `round-outlined-radius-2-stroke-2`(24 坐标系、`stroke-width="2"`) | `viewBox` 与 `strokeWidth` **都省略**——默认值就是这一对 | | 其他任何来源(含 central 的其他 weight/corner) | `viewBox` 与 `strokeWidth` **都显式写** | | 多个子元素 | `content` 包一层 `<>…</>` | **描边属性不写进 `content`。** 它们是 `Glyph` 的字段,由 `Icon` 写到 svg 根上(SVG 的 presentation attribute 沿 DOM 树继承)。往 content 里塞 `stroke="currentColor"` 会让那个 元素不再跟随图标槽的 `text-*` 颜色——`ThinkingState` 的 working 态变色就会失效。 ⚠️ **central 的导出带 `stroke="currentColor"` 和三个描边属性写在 `<path>` 上**,比 Phosphor 官网的复制结果更「脏」。`scripts/central.py` 就是为了这一步存在的,不要手工剥——漏一个 `stroke-width="2"` 在子元素上会覆盖根上的值,而那在代码里看起来完全正常。 **每条都写注释说明图形来自哪里。** 写清 central 的图标名(`aria-label` 的第一段,如 `checkmark-1`)、谁指定的、什么时候、替掉了什么。这个目录的全部价值建立在「每个视觉决定都能 追溯」上,一条没有出处的图形会让下一个人不知道能不能改它。 ### 换掉一档的形状时,检查钉在旧形状上的断言 ⚠️ **不同图标库用的绘制元素不同。** Phosphor 大量用 `<polyline>` 与 `<line>`;**central 几乎 全用 `<path>`**(三横线那种也是三个 `<path>` 而不是三个 `<line>`)。仓库里那些 `querySelector('svg path')?.getAttribute('d')` 式的断言会因此**从 null 变回有值**——方向与 2026-08-27 那次相反,但同样要逐个复核。 它们几乎都该改成 `data-bui-glyph` 语义名——那才是那些断言真正想守的(「这一行用了正确的语义 位」),钉在图形数据上只是当年没有更好的探针。查法: ```bash grep -rn "getAttribute('d')\|querySelector.*svg path\|circle')" src/client --include='*.test.tsx' ``` ⚠️ ⚠️ **测试全绿不代表断言都还有意义。跑完那条 grep 要逐个看,别只修报红的。** 2026-08-27 换套时报红 9 条,修完之后那条 grep 又找出**两条没报红而判据已经失效**的(当时一并修成了语义名, 这里记的是**失效长什么样**,不是现在还坏着): - `ToolNode` 的「图标按工具类型区分」比较两边第一个 `<path>` 的 `d`。当年 Bash 落 `run` 档而 Phosphor 的 terminal 没有 `<path>`,于是一边是 `undefined`、一边是真的 `d`,「不同」成立 ——**它通过是因为一边读不到东西**,不是因为两个图形真的不一样。 - `NoticeNode` 断言 retry 图标的第一个 `<path>` 的 `d` 是 truthy。恰好 `arrows-clockwise` 里有 `<path>` 所以还通过,但换成任何纯 `<line>`/`<polyline>` 的图形都会红,而那时的红是**误报**。 ### 2026-08-28 换 central 时这条 grep 的实际收获 **上一轮改成 `data-bui-glyph` 的那批一条都没红**——语义名探针确实对换图标库免疫,那轮的 判据得到了验证。**唯一要动的是 `ToolChips` 的放大镜**:它当年没改成语义名,而是改成钉**绘制 元素**(「有 `<circle>` 有 `<line>`」),注释里写着「对换到另一套图标库免疫」——而 central 的 放大镜是两个 `<path>`(圆和手柄都不用专门元素画),**证明那句话是错的**。 **教训:「钉绘制元素」比「钉具体 `d`」只强一点点。** 现在它钉的是**拓扑**:两个部件、一个闭合 一个不闭合,`<circle>`/`<line>`/`<path>` 只是画同一个图形的三种手段。 判据(按稳定性从高到低): | 断言问的是 | 用什么探针 | | --- | --- | | 「这里是哪一档」 | `data-bui-glyph` 语义名 ← **默认选这个** | | 「两档图形是否相同」 | svg 的 `innerHTML`(不关心用什么元素画) | | 「这个图形长什么样」 | **拓扑**(几个部件、闭合与否),不是元素名 | 最后一类只在**图形本身就是这一档存在理由**的时候才值得写(放大镜那条是这一类:那一档存在的 全部理由就是「图标要携带信息」)。 ### `CodeBlock` 的图标要透传 `data-bui-md` `bui/CodeBlock` 会作为围栏代码块出现在 `markdown/Prose` 的渲染树里,而那棵树有一条穷尽性断言 要求每个元素都带 `data-bui-md`(`markdown/elements.test.tsx`)。所以那三处 `<Icon>` 要写 `data-bui-md=""`。 图标**内部**的 `<path>` / `<line>` 已经在那条断言里豁免了(2026-08-27 加的,图标根仍受检查) ——判据是「这个属性在说什么」:它声明的是「排版归我们的覆盖表管」,而图标内部没有排版可言。 不要为了讨好那条断言往注册表的 `content` 里加 `data-bui-md`,那会把一个只服务单个消费者的属性 铺到全表。 ## 第 4 步:登记两张对账表 `glyphs.test.tsx` 里两处,缺一个测试就红: 1. **`EXPECTED`** 加图标名。这张表的作用是「悄悄多一档」会被拦住——图标一多就没人记得表里 到底有什么了。 2. **`EXPECTED_RATIO`** 加视觉线宽比值。眼下 13 档全是 `CENTRAL_STROKE_2`(`2 / 24`)这个常量, 所以这张表的作用是**钉住那份齐整**——任何一档偏离都会红,逼着人来说明为什么它该是例外。 混入其他来源时**写成除式而不是小数**(`2 / 24` 而不是 `0.0833`):除式让「这个数字怎么来的」 留在代码里。 ### ⚠️ 默认坐标系从 256 变成 24,那条极值断言的失败方向反了 `glyphs.test.tsx` 第三条断言(「坐标极值与 viewBox 同量级」)原本只有**下界** `> 0.2`, 守的是「24 坐标系的图形漏写 `viewBox` 会继承 256、缩成左上角一个点」。 **默认值改成 24 之后这个失败模式不存在了,反过来的那个出现了**:混进一个 256 坐标系的图形而 漏写 `viewBox`,它会继承 24 —— 坐标爆出画布 10 倍,只看得到左上角一小块。而那时 极值/viewBox宽 ≈ 10.7,**远大于 0.2,下界拦不住**。 所以那条断言必须是**双侧区间**。实测两簇:正确配对时全表落在 0.79–0.96;256 误配 24 时是 10.7 量级。上界取 **1.5** 在两簇之间且留了余量(坐标本不该超出 viewBox,只有描边会溢出, 而那不是 path 数据里的数字)。 ## 第 5 步:验证 按这个顺序,每一步都拦不同的东西: ```bash npx tsc --noEmit # 类型:漏档、name 写错、可选字段访问 npx vitest run src/client/bui/icons # 注册表三条守卫 npx vitest run src/client/bui # 消费点没被改坏 npm run build # 地板;watch 循环不跑 tsc ``` ⚠️ **然后必须真看一眼渲染,明暗两种模式都看。** 图标的问题几乎全是「代码完全正常但画面 不对」那一类,测试拦不住。 最省事的路径是跑这个(前提:`npm run web` 在跑):
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub