Repository Wiki
LyraVoid/Mizuki

原子组件与图标系统

Mizuki 的原子组件层(src/components/atoms/)是整个 UI 体系的最低层积木,其中图标系统(Icon System)是覆盖面最广、实现最复杂的一环:它横跨 Astro 构建时渲染、Svelte 客户端运行时渲染与 Iconify CDN 三条链路,并以统一的封装屏蔽了三条链路的 API 差异。

Purpose and Scope

本页覆盖以下内容:

  • 原子组件目录结构:src/components/atoms/ 下所有原子组件(Badge、Button、Chip、custom-scrollbar、filter-tabs、Icon、Image)的组织方式与"实现 + types.ts + index.ts"三件套约定。
  • 图标系统端到端机制:
    • 原子级 Icon.astro(带 loading 状态与 fallback 的 Web Component 封装器)
    • LocalIcon.svelte(基于本地 @iconify-json/* 包的零 CDN 方案)
    • IconLoader 单例(src/utils/icon-loader.ts,带超时与重试的 Iconify 脚本加载器)
    • 三种标准化图标使用方式及其决策规则(源自 docs/rule/07-icon-usage-specification.md)
  • 失败模式、超时与降级策略:MutationObserver 探测、5 秒兜底超时、3 次重试等。

以下内容有意留给兄弟页面:

  • 分子级/组织级组合组件(components.molecules / components.organisms,它们消费本页的原子组件)
  • 主题与样式令牌体系(Tailwind 主题配置本身)
  • 图标加载插件的 Vite/Astro 集成细节(src/plugins/astro-icon-include.mjs、IconifyLoader.astro 仅在此引用其角色)

Overview

原子组件一览

src/components/atoms/ 目录下的每个原子组件都遵循同一目录约定:实现文件 + types.ts + index.ts:

组件实现文件框架说明
BadgeBadge/Badge.svelteSvelte 5徽标
ButtonButton/Button.astroAstro按钮原子
ChipChip/Chip.svelteSvelte 5芯片标签
CustomScrollbarcustom-scrollbar/CustomScrollbar.astroAstro自定义滚动条
FilterTabsfilter-tabs/FilterTabs.astroAstro过滤标签栏
IconIcon/Icon.astro + Icon/LocalIcon.svelteAstro + Svelte图标(本页重点)
ImageImage/Image.astroAstro图片原子

框架选择的意图很明确:静态、无需交互的原子走 Astro(零 JS 输出),需要响应式状态的原子走 Svelte 5($props / $state / $effect runes)。Icon 是唯一同时存在两种实现的原子——这正是三条渲染链路并存的直接体现。

图标系统的三条链路

项目基于 Iconify 生态,规范文档明确了三种标准化使用方式,并禁止在业务代码中直接使用原生 <iconify-icon> 标签:

#使用方式属性名导入来源适用文件运行时机
①<Icon name="...">nameastro-icon/components.astro 静态组件/页面构建时 (SSR)
②<Icon icon="...">icon@iconify/svelte.svelte 客户端组件客户端运行时 (CSR)
③<Icon icon="...">icon自定义 @components/misc/Icon.astro.astro(需 loading 状态)构建时 + 客户端

name 与 icon 的属性名差异不是笔误,而是两个库的 API 设计差异:astro-icon 模仿 Astro Image 组件的命名约定(构建时内联 SVG),而 iconify-icon / @iconify/svelte 遵循 Iconify 全生态统一规范(浏览器端按需加载图标数据)。

Architecture

Loading diagram...

架构分层意图解读:

  • misc/Icon.astro 作为对外包装器:业务代码面向 @components/misc/Icon.astro,而它内部转发到原子层 atoms/Icon/Icon.astro。这样"原子实现"与"对外契约"解耦——原子组件可以重构而不破坏业务导入路径。
  • Icon.astro 是 Web Component 的薄壳:它并不自己画 SVG,而是输出 <iconify-icon> 自定义元素,并把全部精力放在加载状态管理(loading 指示、成功淡入、超时告警)上。
  • LocalIcon.svelte 走完全独立的本地链路:通过动态 import 本地 @iconify-json/* 包的 icons.json,直接拼出 SVG 字符串,彻底绕开 CDN——为离线/无外部网络依赖场景提供能力。
  • IconLoader 是全局单例:负责把 Iconify 的 CDN 脚本以"超时 + 重试"的方式注入页面,并通过观察者模式广播就绪事件。

核心实现一:Icon.astro —— 带 loading 状态的封装器

Props 契约

astro
1--- 2import type { IconProps } from "./types"; 3 4const { 5 icon, 6 class: className = "", 7 style = "", 8 size = "md", 9 color, 10 fallback = "●", 11 loading = "lazy", 12} = Astro.props as IconProps; 13---

Source: Icon.astro

解构默认值即组件契约的核心:size 缺省为 "md",fallback 缺省为实心圆 "●",loading 缺省 "lazy"。这是"加载失败也有可感知占位"这一设计意图的落点。

尺寸系统:语义化 size → Tailwind text-*

astro
1const sizeClasses = { 2 xs: "text-xs", 3 sm: "text-sm", 4 md: "text-base", 5 lg: "text-lg", 6 xl: "text-xl", 7 "2xl": "text-2xl", 8}; 9 10const sizeClass = sizeClasses[size] || sizeClasses.md; 11const colorStyle = color ? `color: ${color};` : ""; 12const combinedStyle = `${colorStyle}${style}`; 13const combinedClass = `${sizeClass} ${className}`.trim(); 14 15const iconId = `icon-${Math.random().toString(36).substring(2, 9)}`;

Source: Icon.astro

设计意图有三层:

  1. 用 text-* 而非 width/height 控制尺寸:Iconify SVG 使用 1em 相对尺寸,font-size 会自动缩放图标,且能继承父级文本色。sizeClasses[size] || sizeClasses.md 的兜底表达式保证了非法 size 值不会击穿布局。
  2. 颜色走内联 color::SVG 默认 currentColor 填充,所以只需设 color 即可染色,无需侵入图标内部。
  3. 随机 iconId:同一页面会渲染大量 Icon 实例,Math.random().toString(36).substring(2, 9) 生成的 7 位随机 ID 让内联 <script> 能用 data-icon-container 属性选择器精确定位到"自己这个"实例,避免实例间状态串扰。

渲染结构:双 span + 透明度过渡

astro
1<span 2 class={`inline-flex items-center justify-center ${combinedClass}`} 3 style={combinedStyle} 4 data-icon-container={iconId} 5> 6 <span class="icon-loading animate-pulse opacity-50" data-loading-indicator> 7 {fallback} 8 </span> 9 10 <iconify-icon 11 icon={icon} 12 class="icon-content opacity-0 transition-opacity duration-200" 13 data-icon-element 14 loading={loading}></iconify-icon> 15</span>

Source: Icon.astro

外层容器 min-width/height: 1em 防止图标加载期间布局塌陷(CLS)。内层两个子节点互斥显示:icon-loading 初始带 animate-pulse opacity-50 呼吸动画显示 fallback 字符;iconify-icon 初始 opacity-0,加载完成后通过 transition-opacity duration-200 淡入。

客户端加载检测:三重探测机制

js
1function checkIconLoaded() { 2 const hasContent = 3 iconElement.shadowRoot && 4 iconElement.shadowRoot.children.length > 0; 5 6 if (hasContent) { 7 showIcon(); 8 return true; 9 } 10 return false; 11} 12 13showLoading(); 14 15iconElement.addEventListener("load", () => { 16 showIcon(); 17}); 18 19iconElement.addEventListener("error", () => { 20 console.warn(`Failed to load icon: ${icon}`); 21}); 22 23if (window.MutationObserver) { 24 const observer = new MutationObserver(() => { 25 if (checkIconLoaded()) { 26 observer.disconnect(); 27 } 28 }); 29 30 observer.observe(iconElement, { 31 childList: true, 32 subtree: true, 33 attributes: true, 34 }); 35 36 setTimeout(() => { 37 observer.disconnect(); 38 if (!checkIconLoaded()) { 39 console.warn(`Icon load timeout: ${icon}`); 40 } 41 }, 5000); 42} 43 44setTimeout(() => { 45 checkIconLoaded(); 46}, 100);

Source: Icon.astro

这是整个图标系统中最精细的一段状态机,核心难点在于:iconify-icon Web Component 的内容渲染在 shadowRoot 内,且 load 事件在缓存命中时不一定可靠触发。因此实现采用了三重冗余探测:

  1. load 事件监听——正常网络路径的首选信号;
  2. MutationObserver 轮询 shadowRoot——监听 childList/subtree/attributes,一旦 shadowRoot.children.length > 0 立即判定成功并 disconnect(),覆盖事件未触发的边界情况;
  3. 5 秒兜底超时 + 100ms 即时检查——超时断开 observer 并仅打 console.warn(不抛错、不清除 fallback),保证页面不会被单个图标卡死。

该脚本通过 <script is:inline define:vars={{ iconId, icon }}> 注入,define:vars 让服务端生成的随机 ID 与图标名进入客户端闭包——这正是 Astro 中"每实例独立内联脚本"的标准做法。

核心实现二:LocalIcon.svelte —— 本地零 CDN 链路

svelte
1<script lang="ts"> 2 /** 3 * Local Icon component for Svelte 4 * Uses icons from @iconify-json packages installed locally - no CDN required 5 */ 6 interface Props { 7 icon: string; 8 class?: string; 9 } 10 11 const { icon, class: className = "" }: Props = $props(); 12 13 const [collection, name] = $derived( 14 icon.includes(":") ? icon.split(":") : ["mdi", icon], 15 ); 16 17 const iconSetMap: Record<string, string> = { 18 "material-symbols": "@iconify-json/material-symbols", 19 "material-symbols-outlined": "@iconify-json/material-symbols", 20 mdi: "@iconify-json/mdi", 21 "fa7-solid": "@iconify-json/fa7-solid", 22 "fa7-regular": "@iconify-json/fa7-regular", 23 "fa7-brands": "@iconify-json/fa7-brands", 24 "simple-icons": "@iconify-json/simple-icons", 25 }; 26 27 const packageName = $derived(iconSetMap[collection]); 28 let svgContent = $state("");

Source: LocalIcon.svelte

关键设计:

  • 图标名解析:icon.includes(":") ? icon.split(":") : ["mdi", icon]——支持完整写法 "mdi:home",也支持省略集合名的简写(缺省落到 mdi 集合)。
  • iconSetMap 白名单:把 Iconify 集合名映射到本地 npm 包名。material-symbols-outlined 复用 @iconify-json/material-symbols(outlined 是该包内的变体)。未登记的集合 packageName 为 undefined,后续 $effect 直接短路返回——这本身是一种防呆。

动态加载与 SVG 拼装

svelte
1 $effect(() => { 2 const currentIcon = icon; 3 const currentPackageName = packageName; 4 const currentName = name; 5 const currentClassName = className; 6 7 if (!currentPackageName) { 8 return; 9 } 10 11 async function loadIcon() { 12 try { 13 const iconsData = await import( 14 /* @vite-ignore */ `${currentPackageName}/icons.json` 15 ); 16 const icons = iconsData.icons || {}; 17 const iconData = icons[currentName]; 18 19 if (iconData) { 20 const viewBox = iconData.viewBox || "0 0 24 24"; 21 const body = iconData.body; 22 23 if (body) { 24 svgContent = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" class="${currentClassName}">${body}</svg>`; 25 } 26 } 27 } catch (e) { 28 console.warn( 29 `Failed to load icon ${currentIcon} from ${currentPackageName}:`, 30 e, 31 ); 32 } 33 } 34 35 loadIcon(); 36 }); 37</script> 38 39{#if svgContent} 40 {@html svgContent} 41{:else} 42 <span class={className}>●</span> 43{/if}

Source: LocalIcon.svelte

逐行意图:

  • $effect 内先把所有响应式值快照到局部常量(currentIcon 等),是 Svelte 5 中避免依赖追踪范围外泄漏的惯用写法——动态 import() 的模板字符串如果直接引用响应式变量,会扩大依赖收集范围。
  • /* @vite-ignore */ 禁止 Vite 对该动态导入做静态分析/预打包,让包名完全由运行时变量决定。
  • viewBox || "0 0 24 24":icons.json 中部分图标不带 viewBox,24×24 是 Material Design / mdi 的默认坐标系。
  • 渲染端 {#if svgContent} 与 <span>●</span> 分支构成了与 Icon.astro 一致的"成功/占位"语义——整个图标系统的降级哲学是永不抛错、永远有占位。
  • 失败路径仅 console.warn,组件停留在 ● 占位态。

核心实现三:IconLoader 单例 —— 带重试的 CDN 脚本加载器

src/utils/icon-loader.ts 是一个典型的单例 + Promise 去重 + 重试组合,负责把 Iconify 官方 Web Component 脚本可靠地注入页面:

ts
1interface IconifyLoadOptions { 2 timeout?: number; 3 retryCount?: number; 4 retryDelay?: number; 5} 6 7class IconLoader { 8 private static instance: IconLoader; 9 private isLoaded = false; 10 private isLoading = false; 11 private loadPromise: Promise<void> | null = null; 12 private observers = new Set<() => void>(); 13 14 private constructor() {} 15 16 static getInstance(): IconLoader { 17 if (!IconLoader.instance) { 18 IconLoader.instance = new IconLoader(); 19 } 20 return IconLoader.instance; 21 } 22 23 /** 24 * 加载Iconify图标库 25 */ 26 async loadIconify(options: IconifyLoadOptions = {}): Promise<void> { 27 const { timeout = 10000, retryCount = 3, retryDelay = 1000 } = options; 28 29 // 如果已经加载完成,直接返回 30 if (this.isLoaded) { 31 return Promise.resolve(); 32 } 33 34 // 如果正在加载,返回现有的Promise 35 if (this.isLoading && this.loadPromise) { 36 return this.loadPromise; 37 } 38 39 this.isLoading = true; 40 this.loadPromise = this.loadWithRetry(timeout, retryCount, retryDelay); 41 42 try { 43 await this.loadPromise; 44 this.isLoaded = true; 45 this.notifyObservers(); 46 } catch (error) { 47 console.error("Failed to load Iconify after all retries:", error); 48 throw error; 49 } finally { 50 this.isLoading = false; 51 } 52 }

Source: icon-loader.ts

并发语义是这个类最重要的设计点,loadIconify 的三段分支构成一个幂等状态机:

  • 已完成(isLoaded)→ 立即 resolve,零开销;
  • 进行中(isLoading && loadPromise)→ 返回同一个 Promise 引用,而非发起第二次网络请求。这是"Promise 去重"模式:即使页面上几十个图标同时触发加载,网络层也只会有一次脚本注入;
  • 未开始 → 置位 isLoading,进入 loadWithRetry。

finally { this.isLoading = false; } 保证失败后状态复位,下一次调用可以重新尝试;而成功时 isLoaded = true + notifyObservers() 让所有等待方一次性收到广播。

重试与超时算法

ts
1 private async loadWithRetry( 2 timeout: number, 3 retryCount: number, 4 retryDelay: number, 5 ): Promise<void> { 6 for (let attempt = 1; attempt <= retryCount; attempt++) { 7 try { 8 await this.loadScript(timeout); 9 return; 10 } catch (error) { 11 console.warn(`Iconify load attempt ${attempt} failed:`, error); 12 13 if (attempt === retryCount) { 14 throw new Error( 15 `Failed to load Iconify after ${retryCount} attempts`, 16 ); 17 } 18 19 // 等待后重试 20 await new Promise((resolve) => setTimeout(resolve, retryDelay)); 21 } 22 } 23 } 24 25 private loadScript(timeout: number): Promise<void> { 26 return new Promise((resolve, reject) => { 27 // 检查是否已经存在脚本 28 const existingScript = document.querySelector( 29 'script[src*="iconify-icon"]', 30 ); 31 if (existingScript) { 32 // 检查Iconify是否已经可用 33 if (this.isIconifyReady()) { 34 resolve(); 35 return; 36 } 37 } 38 39 const script = document.createElement("script"); 40 script.src = 41 "https://code.iconify.design/iconify-icon/3-latest/iconify-icon.min.js"; 42 script.async = true; 43 script.defer = true; 44 45 const timeoutId = setTimeout(() => { 46 script.remove(); 47 reject(new Error("Iconify script load timeout"));

Source: icon-loader.ts

  • 默认参数:timeout = 10000(单次脚本 10 秒)、retryCount = 3(最多 3 次)、retryDelay = 1000(间隔 1 秒)——最坏情况约 33 秒后放弃。
  • 幂等注入:document.querySelector('script[src*="iconify-icon"]') 先探测 DOM 中是否已有脚本,配合 isIconifyReady() 检查全局对象,避免 SPA 场景下重复插入 <script>。
  • 固定延迟而非指数退避:retryDelay 为常量等待,实现简单,符合"CDN 偶发抖动"的故障模型。

核心流程:一次图标渲染的完整生命周期

Loading diagram...

流程要点:

  1. 构建时(SSR):Icon.astro 输出确定性的 HTML 结构与内联脚本,data-* 属性 + 随机 iconId 构成客户端定位锚点。
  2. 客户端:三重探测(事件 / MutationObserver / 定时器)中任意一个先命中即触发 showIcon();showIcon() 与 showLoading() 是纯互斥的一对状态翻转函数(display + opacity-* 类切换)。
  3. 降级终点统一:无论走哪条失败路径,UI 都停在 fallback ●,且只产生 console.warn——图标缺失被视为降级而非错误。

使用示例

方式①:.astro 静态图标(构建时内联)

astro
1--- 2import { Icon } from "astro-icon/components"; 3--- 4 5<Icon name="material-symbols:arrow-back" class="text-base" /> 6<Icon name={dynamicIconName} class="text-xl" />

Source: 07-icon-usage-specification.md

方式②:.svelte 响应式图标

svelte
1<script lang="ts"> 2 import Icon from "@iconify/svelte"; 3</script> 4 5<Icon icon="material-symbols:pause" class="text-xl" /> 6<Icon icon={dynamicIcon} class="text-lg" />

Source: 07-icon-usage-specification.md

方式③:带 loading/fallback 的自定义 Icon

astro
1--- 2import Icon from "../../misc/Icon.astro"; 3--- 4 5<Icon icon="mdi:react" size="lg" color="#61DAFB" fallback="⚛" />

Source: 07-icon-usage-specification.md

决策规则

规范文档给出了明确的分支决策(原文为 ASCII 流程图,此处归纳):

  • 文件是 .svelte → 一律使用方式②(@iconify/svelte,icon 属性);
  • 文件是 .astro 且需要 loading/fallback 状态 → 使用方式③(@components/misc/Icon.astro);
  • 文件是 .astro 且为常规静态图标 → 使用方式①(astro-icon/components,name 属性)。

反模式(规范明令禁止):在业务代码中直接写原生 <iconify-icon icon="..." width="16" />;混用 name/icon 属性(@iconify/svelte 不识别 name)。

Configuration Options

Icon.astro Props(方式③)

Prop类型默认值说明
iconstring—(必填)Iconify 图标全名,如 "mdi:react"
classstring""追加到容器的额外类名(与尺寸类合并)
stylestring""追加到容器的内联样式(与 color 样式拼接)
size"xs" | "sm" | "md" | "lg" | "xl" | "2xl""md"语义化尺寸,映射为 Tailwind text-*;非法值兜底为 md
colorstring—CSS 颜色值,编译为 color: ...(SVG 以 currentColor 填充)
fallbackstring"●"加载期间/失败时显示的占位字符
loading"lazy" | "eager""lazy"透传给 <iconify-icon> 的加载策略

LocalIcon.svelte Props

Prop类型默认值说明
iconstring—(必填)图标名;含 : 时按 集合:名称 解析,否则默认 mdi 集合
classstring""直接写入生成的 <svg> 标签的 class

IconLoader.loadIconify(options) 选项

选项类型默认值说明
timeoutnumber10000单次脚本注入的超时毫秒数,超时移除 script 并 reject
retryCountnumber3最大尝试次数,耗尽后抛 Failed to load Iconify after N attempts
retryDelaynumber1000每次重试之间的固定等待毫秒数

尺寸对照速查(原生属性 → Tailwind 类)

原生写法替代 CSS 类场景
width="10" height="10"w-2.5 h-2.5 text-[0.625rem]最小图标(节点等)
width="14" height="14"w-3.5 h-3.5 text-xs元信息图标(日期、位置)
width="16" height="16"w-4 h-4 text-base行内小图标(标签、按钮内)
width="20" height="20"text-lg卡片头部图标
width="48" height="48"text-5xl占位大图标
width="64" height="64"text-6xl空状态占位图标

规范推荐优先使用 Tailwind text-* / w-* h-* 类控制大小,而非原生 width/height。

API Reference

Icon.astro(Astro 组件,方式③实现体)

位置:src/components/atoms/Icon/Icon.astro Props:见上一节表格;类型契约定义于 src/components/atoms/Icon/types.ts(IconProps)。 输出:外层 <span data-icon-container> + 内层 loading <span> + <iconify-icon>,附每实例内联脚本。

  • checkIconLoaded(): boolean — 内联函数:检查 shadowRoot.children.length > 0,命中则切换到已加载态并返回 true。
  • showIcon(): void — 隐藏 loading 指示器,icon-content 从 opacity-0 切到 opacity-100。
  • showLoading(): void — 反向切换,回到 loading 态。

LocalIcon.svelte(Svelte 5 组件)

位置:src/components/atoms/Icon/LocalIcon.svelte 导出 Props:icon: string、class?: string。 渲染输出:成功时 {@html svgContent} 内联 SVG;未就绪/失败时 <span class={className}>●</span>。 副作用:$effect 中动态 import("@iconify-json/<集合>/icons.json"),仅在 iconSetMap 含该集合时执行。

IconLoader(src/utils/icon-loader.ts)

  • IconLoader.getInstance(): IconLoader — 私有构造函数 + 静态实例的懒汉式单例入口。
  • loadIconify(options?: IconifyLoadOptions): Promise<void> — 幂等加载 Iconify 脚本;已完成立即 resolve,进行中复用同一 Promise;成功后置 isLoaded 并 notifyObservers();重试耗尽后 throw Error("Failed to load Iconify after N attempts")。
  • loadWithRetry(timeout, retryCount, retryDelay): Promise<void>(private)— 顺序重试循环,末次失败抛聚合错误。
  • loadScript(timeout): Promise<void>(private)— DOM 注入 https://code.iconify.design/iconify-icon/3-latest/iconify-icon.min.js,先探测既有 script 与全局就绪状态;超时移除节点并 reject。

Failure Modes, Edge Cases & Concurrency

场景机制结果
CDN 图标加载失败<iconify-icon> error 事件console.warn("Failed to load icon: ..."),保持 ● 占位,不抛异常
CDN 响应但 load 事件未触发MutationObserver 监听 shadowRoot childList观察到内容即切换已加载态并 disconnect()
加载超过 5 秒5s setTimeout 兜底断开 observer,仅 console.warn("Icon load timeout: ...")
非法 size 值sizeClasses[size] || sizeClasses.md兜底为 md,不击穿布局
LocalIcon 收到未登记集合packageName 为 undefined,$effect 提前 return组件停在 ● 占位
LocalIcon 动态 import 失败catch (e) → console.warn停在 ● 占位
多个图标并发触发脚本加载loadIconify 返回同一 Promise 引用网络层仅一次脚本注入(Promise 去重)
loadIconify 首次失败finally { isLoading = false }状态复位,下次调用可重试
同页多 Icon 实例随机 iconId + data-icon-container 选择器各实例脚本只操作自己的 DOM,互不串扰
图标加载期间布局容器 min-width/height: 1em占位与最终图标等基线尺寸,避免 CLS

并发与一致性要点:Icon.astro 每实例独立脚本意味着 N 个图标就是 N 个 MutationObserver——代码通过"命中即 disconnect() + 5 秒绝对上限"约束了观察器的生命周期,避免泄漏。IconLoader 的 observers: Set<() => void> 支持多个等待方订阅同一就绪事件,配合单例语义保证全局只有一个加载权威源。

Performance & Operational Notes

  • 构建时 vs 运行时的取舍:方式①(astro-icon)在构建期内联 SVG、零网络请求,是性能最优解,因此规范将其设为 .astro 的默认选择;方式③仅在确需 loading/fallback 时使用——这是"按需付出运行时成本"的明确设计取向。
  • LocalIcon.svelte 的零 CDN 特性:@vite-ignore 动态 import 让图标数据随本地包分发,适合对第三方 CDN 不可达敏感的部署环境;代价是 icons.json 体积由打包器按需切分承担。
  • CDN 版本钉扎方式:脚本 URL 使用 3-latest(主版本 latest),获得补丁级自动更新同时锁定大版本 API 兼容。
  • 可观测性:所有失败路径均走 console.warn(除 loadIconify 最终失败用 console.error 并 throw),便于在浏览器控制台直接定位是哪一个图标名出错——icon 变量被闭包捕获进告警消息。

Extension Points

  • 新增图标集合(本地链路):在 LocalIcon.svelte 的 iconSetMap 中登记 "集合名": "@iconify-json/集合名",并安装对应 npm 包即可,无需改动其他代码。
  • 新增尺寸档位:扩展 Icon.astro 的 sizeClasses 映射表(如 "3xl": "text-3xl")。
  • 更换/自托管 CDN:icon-loader.ts 中脚本 URL 为单一注入点;配合 src/constants/icon.ts(图标相关常量)可集中调整。
  • 新增原子组件:遵循目录三件套约定——实现文件 + types.ts(Props 契约)+ index.ts(桶导出),框架选择按"是否需要响应式状态"决定 Astro 或 Svelte。

Tests

本页未在仓库中定位到针对图标原子的独立测试文件(源码探索预算内未发现 *.test.* / *.spec.* 匹配项)。规范文档 docs/rule/07-icon-usage-specification.md 承担了"用法契约测试"的角色:其中列出已修复的历史错误(属性名混用 name/icon、直接使用原生 <iconify-icon> 标签),可作为回归检查清单。

Sources

(4 files)