跳到正文
博客园· 239573049·· 3 小时前AI 评分55

作者用一天时间将 Arc Web 组件库迁移为 Avalonia 原生组件库 TKCode.Arc

一天时间将一个web组件库完全迁移成(Avalonia)原生组件库 - 239573049

AI 导读

作者 token 称用一天时间把 Arc 的 110 个免费 Web 组件完整移植为原生 Avalonia 组件库 TKCode.Arc,基于 AtomUI 构建,不含 WebView,已开源并以 0.1.1-preview.1 发布到 NuGet。

正文

我花了一天时间,将 Web 组件库完整移植成原生 Avalonia 组件库,提供跟 Web 完全一样的体验

大家好,我是 token,最近我开始实验我的 AI Coding 方案。在此之前我已经落地了很多商业级产品,这次我尝试验证:使用 AI 完整复刻 Web 端的组件库到 Avalonia。这次可以造福各大 Avalonia 玩家,并且组件库完全开源:https://github.com/AtomUICommunity/TKCode.Arc

下面是组件库的一些效果展示。


先看效果

先来一个最直观的:整页主题切换。点一下按钮,新的外观像日食一样掠过整个页面。这是 ThemeSwitcher 的 Eclipse 变体,另外还有 Reveal(从按钮处圆形扩散)、Split(中线接缝展开)、Rise(底部升起)。

00-hero-theme-eclipse

原生 Avalonia 渲染,没有 WebView,没有任何网页。

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

01-compare-web-vs-avalonia

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


这个库是什么

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:像橡皮筋一样的滑块

滑块拖过两端时会像橡皮筋一样被拉伸,数值标签带着摇摆,松手后依松开时的速度弹回,中间还有会"卡入"的档位。
03-elastic-slider

<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:滑动显示行操作

邮件列表的经典交互:向右滑出"标为未读",向左滑出"归档"和"删除"。松手后,行会带着松手瞬间的速度,用弹簧停靠到最近的停靠点;快速轻扫也会滑向对应方向的停靠点。
04-swipe-actions

<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 则是一个会"变形"的按钮:点击后原地变成确认、加载中、带撤销的结果。
05-hold-to-confirm
06-confirm-morph

<arc:HoldToConfirm Label="Hold to delete project" ConfirmedLabel="Deleted" />

<arc:ConfirmMorph Label="Delete project" Icon="{x:Static arc:ArcIcons.Trash2}" />

ImageCompare:拖动对比

拖动分隔线横跨两张图片,看修图前后的差异。
07-image-compare


二、图表库:13 个图表组件,一个不落

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

亮色与暗色下的 13 个图表:
image
image

趋势

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

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

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

chart-brush

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

chart-streamgraph

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

chart-sparkline

构成

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

chart-donut

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

chart-waffle

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

chart-treemap

<arc:Treemap Label="Revenue" ColorLabel="Growth" FocusedNodeChanged="OnFocusedNodeChanged" />

Gauge 仪表盘:在已知范围内显示一个数值,按阈值着色(Healthy / Filling up / Critical),数值变化时指针滚动。

chart-gauge

分布与对比

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

Ridgeline

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

chart-ridgeline

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

chart-heatmap

数字

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

chart-animated-counter


三、AI 应用的刚需:聊天与流式 Markdown

现在做 AI 应用,聊天界面和流式渲染几乎是标配。ChatThread 自带分组消息、表情回应、已读回执、输入状态和带附件的输入区;Markdown 控件可以渲染流式回复,代码块用 Arc 的配色做 TextMate 语法高亮。

下面是点一下"Stream a reply"之后,一段 Markdown 回复流出的样子:新到的文字会在 240 毫秒内淡入,节奏控制避免文字成批涌现。

11-chat-streaming-markdown

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 里还有"一千条消息"的聊天示例,用来验证长会话的滚动:

12-chat-thousand-messages


四、表单与编辑器

RichTextEditor

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

13-rich-text-editor

DateRangePicker

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

14-date-range-picker

ColorPicker 与 MentionInput

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

15-color-picker

16-mention-input


五、浮层与布局

BottomSheet:跟手的底部面板

停靠在预览高度或完整高度,跟着手指移动;松手后停靠到最近的档位,向下拖则关闭,快速滑动至少会移动一个档位。

17-bottom-sheet

<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 直接拖放整理条目。

18-toast-stack

19-reorderable-list


六、Blocks:整页级别的组合

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

20-blocks-command-palette
21-blocks-settings-page


怎么做到的

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 三级,一行代码切换:

ArcMotion.SetLevel(window, ArcMotionLevel.Reduced);   // Full、Reduced 或 Off

这里踩过一个坑:Avalonia 12.1.4 的 ITransition 没法由外部实现(TransitionBase.Apply 是 internal),所以"按控件保存速度的声明式弹簧过渡"做不出来。最后的取舍是:声明式的过渡用"烘焙成曲线的弹簧",被打断时从零速度重启;需要保留速度的(手势、连续重定向、数字滚轮)一律走 MotionValue<T>。

3. Token 驱动的主题

全局语义 Token 由 ArcDesign 承载,强调色是它的 ColorPrimary。换主题色、换亮暗外观都是几行代码:

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):

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 图标集

然后在应用启动处注册:

AppBuilder.Configure<App>()
    .UsePlatformDetect()
    .WithInterFont()
    .WithArcFonts()          // 可选:Geist 展示字体和 Geist Mono 代码字体
    .UseAtomUI(builder =>
    {
        builder.UseDesktopControls();
        builder.UseArc();
    });
<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:

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

来源:博客园 · cnblogs.com