我花了一天时间,将 Web 组件库完整移植成原生 Avalonia 组件库,提供跟 Web 完全一样的体验
大家好,我是 token,最近我开始实验我的 AI Coding 方案。在此之前我已经落地了很多商业级产品,这次我尝试验证:使用 AI 完整复刻 Web 端的组件库到 Avalonia。这次可以造福各大 Avalonia 玩家,并且组件库完全开源:https://github.com/AtomUICommunity/TKCode.Arc
下面是组件库的一些效果展示。
先看效果
先来一个最直观的:整页主题切换。点一下按钮,新的外观像日食一样掠过整个页面。这是 ThemeSwitcher 的 Eclipse 变体,另外还有 Reveal(从按钮处圆形扩散)、Split(中线接缝展开)、Rise(底部升起)。

原生 Avalonia 渲染,没有 WebView,没有任何网页。
那"跟 Web 一样"到底有多一样?我把原版 Arc 的页面和 Avalonia 版放在一起,同一个组件、同样的状态,左边是 Web,右边是 Avalonia:

字体也用的是同款:展示字体 Geist、代码字体 Geist Mono(随 TKCode.Arc.Fonts 提供),正文用 Inter。亮色和暗色两套外观,以及 8 种强调色预设(Neutral、Violet、Blue、Green、Amber、Orange、Coral、Rose),都是现成的:

这个库是什么
TKCode.Arc 是 Arc 免费组件的 Avalonia 移植版,基于 AtomUI 构建。Arc 原版是 React + CSS 变量 + Motion,我要做的是:视觉、交互、键盘行为和动效都对齐,但全部落在原生控件上。
| 项目 | 数据 |
|---|---|
| 组件 | 110 个,分布在 22 个分组(目录结构和 Arc 侧边栏一一对应) |
| 可运行示例 | 460 多个(每个示例都能在 Gallery 里直接看效果和源码) |
| 测试 | 1400 多个测试方法(纯逻辑测试 + 无头 Avalonia 测试) |
| 代码量 | 约 22 万行 C#(含 XML 文档注释,不含生成代码)、约 2.5 万行 AXAML |
| 文档 | 每个组件都有简体中文、繁体中文、英文三份文档 |
| 图标 | 内置 187 个 Lucide 图标,另有完整的 Lucide 图标包(2155 个) |
| 平台 | Windows / macOS / Linux 桌面,net10.0 与 net11.0 |
| NuGet | 最新预览版 0.1.1-preview.1:TKCode.Arc、TKCode.Arc.Core、TKCode.Arc.Fonts、TKCode.Arc.Icons.Lucide |
| 许可证 | MIT |
| 在线体验 | https://docs.tkcode.si/ |
在线体验站点:https://docs.tkcode.si/,不用装任何东西,浏览器里就能试每个组件。
支持的组件清单
Arc 侧边栏的 22 个分组、110 个免费组件,已经全部移植:
| 分组 | 组件 |
|---|---|
| 按钮(10) | Button、ActionButton、SplitButton、ButtonGroup、FloatingButtonGroup、ExpandingButtonGroup、CopyButton、IconMorph、ConfirmMorph、ThemeSwitcher |
| 手势(3) | HoldToConfirm、SwipeActions、Reactions |
| 菜单(3) | DropdownMenu、ContextMenu、UserMenu |
| 文本输入(7) | Input、Textarea、PasswordField、PasswordStrength、SearchField、ExpandingSearch、InlineEdit |
| 特殊输入(7) | NumberField、MoneyInput、PhoneInput、ScrubInput、TagInput、MentionInput、ShortcutRecorder |
| 选择器(5) | Select、Combobox、MultiSelect、MorphSelect、ChipGroup |
| 开关与选项(6) | RadioCards、BillingToggle、Checkbox、RadioGroup、Switch、SegmentedControl |
| 滑块(2) | Slider、ElasticSlider |
| 选取器(5) | Calendar、DatePicker、DateRangePicker、TimePicker、ColorPicker |
| 编辑器(3) | RichTextEditor、SignaturePad、FileDropzone |
| 导航(3) | Tabs、Breadcrumb、Pagination |
| 展开与滚动(5) | ScrollArea、Accordion、ExpandableCard、ResizablePanels、ExpandableText |
| 浮层(6) | Dialog、Drawer、BottomSheet、Popover、HoverCard、Tooltip |
| 消息(5) | Alert、Toast、ToastStack、AnnouncementBar、CookieConsent |
| 进度(5) | Progress、Skeleton、Stepper、Countdown、UsageMeter |
| 头像与徽标(3) | Avatar、AvatarGroup、Badge |
| 卡片(3) | Card、MetricCard、EmptyState |
| 图表(13) | LineChart、BarChart、DonutChart、Streamgraph、BrushChart、WaffleChart、SlopeChart、Sparkline、Gauge、ActivityHeatmap、AnimatedCounter、Ridgeline、Treemap |
| 表格与数据(6) | SortableDataTable、TreeView、ReorderableList、FilterToolbar、CodeBlock、JsonViewer |
| 动态与会话(3) | Timeline、CommentThread、ChatThread |
| 媒体(3) | ImageCompare、Carousel、CardStack |
| 文字效果(4) | TextReveal、InViewTitle、TextMorph、TextShimmer |
在 Arc 的组件之外,仓库里还有这些 TKCode 自己做的内容:
- Markdown:流式渲染 Markdown,代码块用 Arc 的配色做 TextMate 语法高亮。
- 插图(4):DataFlow、PaymentFlow、MultiRegionFailover、VectorSearch,带动画的场景控件。
- Blocks(12):Sidebar、PageHeader、CommandPalette、SearchResults、SettingsPage、SecuritySettings、BillingOverview、UsageBilling、CancelFlow、SettingsCommand、NotificationCenter、EmptyStates。
- AdvancedCharts:DataGrid(电子表格式的数据网格)。
其中 Blocks 与 AdvancedCharts 目前只能从仓库构建,还没有发布到 NuGet。
下面挑一些我觉得最能说明问题的组件。图表库单独放在第二节,13 个图表每一个都录了动图。
一、手感:弹簧动效
Arc 最让人舍不得的是动效手感:可以随时打断、会保留速度、拖过头会回弹。这些在 Avalonia 里没有现成的东西,我自己实现了一套弹簧引擎(后面会讲)。先看效果。
ElasticSlider:像橡皮筋一样的滑块
滑块拖过两端时会像橡皮筋一样被拉伸,数值标签带着摇摆,松手后依松开时的速度弹回,中间还有会"卡入"的档位。

xml
<arc:ElasticSlider Label="Volume" Value="40" Width="360">
<arc:ElasticSlider.StartIcon>
<arc:ArcGlyph Data="{x:Static arc:ArcIcons.Minus}" Size="16" />
</arc:ElasticSlider.StartIcon>
<arc:ElasticSlider.EndIcon>
<arc:ArcGlyph Data="{x:Static arc:ArcIcons.Volume2}" Size="16" />
</arc:ElasticSlider.EndIcon>
</arc:ElasticSlider>
SwipeActions:滑动显示行操作
邮件列表的经典交互:向右滑出"标为未读",向左滑出"归档"和"删除"。松手后,行会带着松手瞬间的速度,用弹簧停靠到最近的停靠点;快速轻扫也会滑向对应方向的停靠点。

xml
<arc:SwipeActions Label="Inbox" Width="440">
<arc:SwipeActions.LeadingActions>
<arc:SwipeAction Label="Unread" Icon="{x:Static arc:ArcIcons.Mail}" Tone="Accent" KeepsRow="True" />
</arc:SwipeActions.LeadingActions>
<arc:SwipeActions.TrailingActions>
<arc:SwipeAction Label="Archive" Icon="{x:Static arc:ArcIcons.Archive}" />
<arc:SwipeAction Label="Delete" Icon="{x:Static arc:ArcIcons.Trash2}" Tone="Danger" />
</arc:SwipeActions.TrailingActions>
</arc:SwipeActions>
HoldToConfirm 与 ConfirmMorph:危险操作的确认
HoldToConfirm 要求长按才能确认破坏性操作,也支持键盘(聚焦后按住空格或回车)。ConfirmMorph 则是一个会"变形"的按钮:点击后原地变成确认、加载中、带撤销的结果。


xml
<arc:HoldToConfirm Label="Hold to delete project" ConfirmedLabel="Deleted" />
<arc:ConfirmMorph Label="Delete project" Icon="{x:Static arc:ArcIcons.Trash2}" />
ImageCompare:拖动对比
拖动分隔线横跨两张图片,看修图前后的差异。

二、图表库:13 个图表组件,一个不落
图表是移植里最费功夫的一块:每个图表都有入场动画、数据切换时的"变形"过渡、悬停读数和键盘操作。下面 13 张动图由脚本驱动 Gallery 里的真实示例(鼠标移动、点击、拖拽都是真实的输入事件),逐帧录制,没有后期合成。
亮色与暗色下的 13 个图表:


趋势
LineChart 折线图 :多系列折线,十字准线平滑跟随鼠标读数,切换 7 天 / 30 天 / 90 天时路径变形。

BarChart 柱状图 :逐根读数,标题数字会滚动;从 Week 切到 Month,柱子按 key 变形。

BrushChart 带概览条的时间序列:拖动概览条里的窗口缩放,调整手柄改变范围,事件标注在原处查看。

Streamgraph 流图:在摆动基线上堆叠的流层,可以隔离某一层逐周读取;12 / 26 / 52 周与 Wiggle / Silhouette / Zero 基线之间变形。

Sparkline 迷你趋势图:嵌进 KPI 卡片的紧凑趋势,悬停读数。

构成
DonutChart 环形图:悬停图例行,中心数值跟着变化;切换数据集时扇区变形,点击图例行可以开关扇区。

WaffleChart 华夫图:十乘十的单位图,每个方格代表 1%;数据变化时方格会飞到各自的新分组。

Treemap 矩形树图 :悬停读数,点击区域下钻,Esc 或面包屑返回;切换指标(Revenue / Seats)时,每个色块平滑过渡到新的尺寸。

xml
<arc:Treemap Label="Revenue" ColorLabel="Growth" FocusedNodeChanged="OnFocusedNodeChanged" />
Gauge 仪表盘:在已知范围内显示一个数值,按阈值着色(Healthy / Filling up / Critical),数值变化时指针滚动。

分布与对比
Ridgeline 山脊图:重叠的分布,每组一条山脊;悬停抬起山脊并读出四分位数,切换数据集(Zurich / Lisbon)时每条曲线变形。

SlopeChart 斜率图:两个坐标轴上的前后对比,线条逐条绘出,数值旁显示排名变化,切换数据集时每条线滑到新的斜率。

ActivityHeatmap 活动热力图:一整年的活动,每天一个方块;悬停看当天数据,点击选中,切换年份。

数字
AnimatedCounter 数字滚动:总数变化时每一位数字像滚轮一样滚到新值。

三、AI 应用的刚需:聊天与流式 Markdown
现在做 AI 应用,聊天界面和流式渲染几乎是标配。ChatThread 自带分组消息、表情回应、已读回执、输入状态和带附件的输入区;Markdown 控件可以渲染流式回复,代码块用 Arc 的配色做 TextMate 语法高亮。
下面是点一下"Stream a reply"之后,一段 Markdown 回复流出的样子:新到的文字会在 240 毫秒内淡入,节奏控制避免文字成批涌现。

csharp
var reply = new Markdown();
reply.Append("Here is the **fix**:\n\n```csharp\n");
reply.Append("var total = items.Sum(i => i.Price);\n```\n");
reply.Complete();
Gallery 里还有"一千条消息"的聊天示例,用来验证长会话的滚动:

四、表单与编辑器
RichTextEditor
轻量富文本编辑器:边写边输入 Markdown,输入 / 唤出斜杠菜单插入块,选中文字时出现浮动工具栏,可以输出 HTML 和 Markdown。

DateRangePicker
从触发器展开成两个月份面板,带预设项,选区的高亮会随着鼠标伸展。

ColorPicker 与 MentionInput
ColorPicker 支持格式切换、吸管、已保存色块和对比度读数。MentionInput 支持 @人员 和 #频道,提及作为整体令牌处理,建议列表出现在插入符旁边。


五、浮层与布局
BottomSheet:跟手的底部面板
停靠在预览高度或完整高度,跟着手指移动;松手后停靠到最近的档位,向下拖则关闭,快速滑动至少会移动一个档位。

xml
<arc:Button Content="Trip details" Command="{Binding #TripSheet.OpenCommand}" />
<arc:BottomSheet x:Name="TripSheet" Title="Trip to Lisbon" Description="3 nights · 12--15 June">
<!-- 内容 -->
</arc:BottomSheet>
ToastStack 与 ReorderableList
ToastStack 把结果提示堆叠在边缘,需要时展开;ReorderableList 直接拖放整理条目。


六、Blocks:整页级别的组合
除了单个组件,我还基于这些组件搭了一批整页级的 Blocks(命令面板、设置页、通知中心、用量与账单等),放在 TKCode.Arc.Blocks 项目里(目前只能从仓库构建,还没有发布到 NuGet)。


怎么做到的
1. 先写设计,再写代码
第一件事不是写控件,而是写设计文档:把 Arc 的 22 个分组、110 个组件、设计 Token、动效规则整理成 design/ 目录,并把每个关键决策写成 ADR(一共 14 篇):依赖怎么选、命名怎么定、动效引擎怎么做、哪些不做。AI 写代码最怕的是"每次都重新发明约定",所以我另外写了一份实现手册和一份 AGENTS.md,把硬规矩列清楚,例如:
- 每个公开控件必须有自己的 Design Token 类;
- 颜色、圆角、阴影、字体只能从
{arc:ArcDesignTokenResource ...}取; - 动效只能声明在 ControlTheme 里,代码驱动的动效必须说明为什么 ControlTheme 表达不了;
- 不用反射,AOT 与裁剪安全;
- 每个组件必须有三种语言的文档页。
2. 自研弹簧动效引擎
Arc 的动效建立在 Motion(framer-motion)的弹簧之上,规则很明确:一切可打断、尺寸跟随弹簧、数字滚动到新值、首帧不播放动画、每个动画都有减少动效分支。
但 Avalonia 12 自带的 SpringEasing 只是一条按动画时长归一化的曲线,既不会按物理过程自行收敛,被打断时也保留不了速度。所以我在 TKCode.Arc.Core 里做了:
SpringSolver:阻尼振子的解析解,覆盖欠阻尼、临界阻尼、过阻尼;MotionValue<T>:可打断,重定向时保留当前位置和速度;MotionScheduler:每个 TopLevel 一个,基于RequestAnimationFrame批量推进,空闲时不请求帧;- 弹簧过渡可以直接写在 ControlTheme 里,首帧不动画靠
:motion-full/:motion-reduced伪类实现。
动效分 Full / Reduced / Off 三级,一行代码切换:
csharp
ArcMotion.SetLevel(window, ArcMotionLevel.Reduced); // Full、Reduced 或 Off
这里踩过一个坑:Avalonia 12.1.4 的 ITransition 没法由外部实现(TransitionBase.Apply 是 internal),所以"按控件保存速度的声明式弹簧过渡"做不出来。最后的取舍是:声明式的过渡用"烘焙成曲线的弹簧",被打断时从零速度重启;需要保留速度的(手势、连续重定向、数字滚轮)一律走 MotionValue<T>。
3. Token 驱动的主题
全局语义 Token 由 ArcDesign 承载,强调色是它的 ColorPrimary。换主题色、换亮暗外观都是几行代码:
csharp
var config = new ThemeConfigBuilder()
.WithAlgorithms(ThemeAlgorithm.Default, ThemeAlgorithm.Dark)
.WithArc(arc => arc.Accent(ArcAccentPreset.Violet))
.Build();
await Application.Current!.GetThemeManager()!.ApplyThemeAsync(
new ThemeRequest(IThemeManager.DEFAULT_THEME_ID, config, ThemeTransitionReason.UserRequest));
4. AI Coding 的工作方式
这次实验,核心是把 AI 当成一个需要"流程和护栏"的团队,而不是一个聊天框:
- 并行,但隔离。 同时让多个子 Agent 写组件时,我给每个 Agent 一份工作区快照,完成后再三方合并回主干。试过 git worktree 和共用同一个目录,前者拿不到未提交的改动,后者一个 Agent 写了一半的文件会让所有人的构建一起红。
- 体力活交给小模型,但必须机械校验。 111 个组件页 × 3 种语言的文档,用小模型批量写,然后用生成器和测试做机械校验:它偶尔会悄悄改写文本,或者编造、漏掉 API 成员。
- 让 AI 自己"看"。 给实现者一个无头渲染截图工具,要求它检查亮色、暗色、窄屏三种截图,大部分视觉 bug 是这样抓到的。
- 测试兜底。 1400 多个测试方法,其中包含检查每个组件三语言文档是否齐全的测试。
- 接受工程上的不完美。 并发太多会触发限流,所有 Agent 同时停下;批量任务会遇到偶发的认证失败,分小批重试就好。这些都是流程问题,不是模型能力问题。
关于"完全一样"的边界
标题里的"完全一样",指的是 Arc 免费组件在视觉、交互、键盘行为和动效上的对齐,我用上面那种并排对照来验证。也有明确的边界:
- 只移植 Arc 的 110 个免费组件;Arc 的 Pro 组件受许可约束,不复制它们的代码、样式和文档。
- 目前只支持桌面(Windows / macOS / Linux);移动端需要的 AtomUI 移动控件还不存在。
- 组件库本身不发布 Browser(WebAssembly)版;只有 Gallery 提供 WebAssembly 宿主。
Button、Calendar、Carousel、ContextMenu、DatePicker、Slider、SplitButton、TabItem、TimePicker、TreeView等与Avalonia.Controls同名,同一文件里同时引用时需要给其中一个命名空间设别名。- 依赖 AtomUI 6.2.6 与 Avalonia 12.1.4,版本是精确锁定的。
- 目前是预览版(
0.1.1-preview.1),版本号带-preview后缀,各版本之间 API 仍可能变化。 - NuGet 上发布的是
TKCode.Arc、TKCode.Arc.Core、TKCode.Arc.Fonts、TKCode.Arc.Icons.Lucide四个包;TKCode.Arc.Blocks与TKCode.Arc.AdvancedCharts暂时需要从仓库构建。
快速开始
先从 NuGet 安装(目前最新预览版是 0.1.1-preview.1):
bash
dotnet add package TKCode.Arc --version 0.1.1-preview.1
dotnet add package TKCode.Arc.Fonts --version 0.1.1-preview.1 # 可选:Geist 与 Geist Mono 字体
dotnet add package TKCode.Arc.Icons.Lucide --version 0.1.1-preview.1 # 可选:完整的 Lucide 图标集
然后在应用启动处注册:
csharp
AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.WithArcFonts() // 可选:Geist 展示字体和 Geist Mono 代码字体
.UseAtomUI(builder =>
{
builder.UseDesktopControls();
builder.UseArc();
});
xml
<Window xmlns:arc="https://github.com/AtomUICommunity/TKCode.Arc">
<StackPanel Spacing="12">
<arc:Button Content="Save changes" />
<arc:Button Content="Cancel" Variant="Secondary" />
<arc:Switch Content="Email notifications" IsChecked="True" />
<arc:CopyButton Value="npx arc add copy-button" />
<arc:Calendar ShowToday="True" />
</StackPanel>
</Window>
不想装的话,直接打开在线体验站点 https://docs.tkcode.si/ 。想在本地跑一遍 Gallery:
bash
dotnet run --project gallery/TKCode.Arc.Gallery.Desktop -f net10.0
最后
0.1.1-preview.1 已经发布到 NuGet。这次实验让我确认了一件事:只要有清晰的设计文档、明确的规则和可机械验证的流程,AI 可以把一个体量很大的 Web 组件库,完整地搬到另一个技术栈上。接下来我会继续完善它,欢迎 Avalonia 的朋友们试用、提 Issue、提 PR:
GitHub:https://github.com/AtomUICommunity/TKCode.Arc
在线体验站点:https://docs.tkcode.si/
如果觉得有用,点个 Star 就是对我最大的鼓励。
致谢: 感谢 Arc(MIT)的设计与动效、AtomUI 的基础设施、Lucide、Geist 与 Inter 字体。第三方许可信息见仓库中的 THIRD-PARTY-NOTICES.md。
Avalonia交流群:922379598