Changelog

更新日志

两个包独立发版:@hulianui/ui 提供组件,@hulianui/tokens 提供设计令牌 CSS。记录遵循语义化版本并由 changesets 生成。

当前版本

v0.22.0
npmGitHub Releases
  1. v0.22.0

    @hulianui/ui新功能

    MathText 补齐高中学段记号:向量箭头、黑板粗体数集、集合/逻辑符号、LaTeX 转义字符

    上一轮符号表是按 22k 字符的初中题面频次建的,方法没问题,样本口径偏窄。消费方拿 1324 道题(题干 + 解析,含小学到高中)重做了一遍统计,向量与集合/逻辑记号是高中的主力,初中样本里几乎不出现 —— 于是它们全落在表外,在页面上直接显示成反斜杠原文。

    向量箭头 `\vec` / `\overrightarrow`#83

    这两个合计出现 282 次,在整张频次表里排第 3,比已经支持的 \overline(5 次)高 56 倍。此前 DECORATE_COMMANDS 只有 overlinehat 两档,取不到值就按字面输出(这一步本身是对的,符合「不认识的记号不吞掉」),只是这两个应该被认识。

    新增 arrow 一档。箭头宽度跟随内容:杆是可拉伸的 border,箭头尖是不变形的 SVG,所以 \vec{a} 是短箭头、\overrightarrow{AB} 自动盖住两个字母。TeX 里 \vec 是定宽短箭头、\overrightarrow 才满宽,这个差异被有意抹平 —— 题面场景下两个记号都指向量,宽度不携带信息,而自适应能让 \vec{AB} 这种写法也盖得住。箭头是绝对定位的覆盖层,不撑高行盒,与分数一样不会打乱中文正文的行距。

    tsx
    <MathText>{"已知 \\overrightarrow{AB} 与 \\vec{a} 共线"}</MathText>

    `\mathbb{}` 映射黑板粗体,不是剥壳#84

    \mathbb{R} → ℝ,26 个大写字母全覆盖(C/H/N/P/Q/R/Z 用 BMP 的字母式符号,其余落 SMP 数学字母区)。刻意不做成「剥掉外壳留下裸字母」:题面里实数集 ℝ 与变量 R 是两个东西,剥成同一个字母后「定义域为 ℝ」读起来就像「定义域为 R」,而且没人看得出信息已经丢了。参数里认不出的字符逐个原样保留,\mathbb{R+}ℝ+,不会因为一个 + 就整体放弃。

    LaTeX 转义字符 `\{` `\}` `\%` `\$` `\&` `\#` `\_`

    集合构建式 \{x \mid x>0\} 里的花括号此前会连着反斜杠一起显示出来 —— \mid 补了表也没用,因为两侧还露着 \{ \}。与符号表不同,转义字符是一个有限闭集合而非长尾,所以整套补齐、不按频次裁。

    其余按实测频次补入的命令

    \Leftrightarrow ⇔(充要条件,10 次)·\to →(极限,4 次)·\mid ∣(集合构建式,4 次)·\backsim ∽·\varphi φ·\Gamma Γ·\langle ⟨ \rangle ⟩(内积)·\forall ∀·\frown ⌢

    另新增两个取参数的命令:\underline{} 给已有内容加下划线(与填空槽是两回事,后者是空位);\overset{}{} 把上方记号叠在内容上,\overset{\frown}{AB} 即弧 AB —— 这是弧的规范写法,\frown{AB} 在 LaTeX 里是「弧符号紧跟一个分组」,本组件仍按字面渲染成 ⌢{AB},看着不对正是设计意图,好过猜一个上游没表达的意思。

    \overset 的上方记号在 mathToPlain 里会被保留⌢AB),这一点与 \overline / \vec 不同:后者是纯样式线,没有对应字符;前者的上方是有语义的内容,检索时丢掉就少了东西。

    报告里有三条不成立,这里说明一下

    \Rightarrow(52 次)、\mathbf{}(8 次)、\quad(8 次)在 0.20.0 里就已经支持,逐个实测过。另外报告提到「剥 \mathbf{} 外壳要小心命令边界,\cdot\mathbf{b} 直接剥会变成不存在的 \cdotb」—— 这个坑在本组件里不存在:解析器是从左到右逐命令消费的,不是字符串替换,\cdot 在遇到 \mathbf 之前就已经被消费成 · 了。那是消费方在自己的入库归一化里做字符串替换才会踩的坑。

    顺带修掉的一处性能问题:`MathText` 现在是 `memo` 的

    MathText 每次渲染都要把整条题面重新 parseMath 一遍,而它此前不是 memo 的 —— 父级任何一次无关更新(题库页面上通常是筛选、分页、选中态这类),一屏几十个实例就会全部重新解析一遍。性能扫描在「父组件更新但 props 不变」这一步实测到 3 次可避免的重渲染,加 memo 后归零。props 全是原始值(children 是字符串),浅比较就够;locale 走 context,语言切换仍会正常更新。同库的 Markdown 早就是这么做的,这次只是把 MathText 补齐。

    顺带修掉的一处文档缺陷

    MathTextQuestionCard 的中文文档把「禁忌 / 坑」章节的标题写成了「坑」,而 conventions 生成器认的是前者。结果是这两个组件的中文注意事项从来没进过 `conventions.json` —— 英文侧一直是全的,中文侧是 0 条,通过 MCP 查约定的中文用户看不到它们。标题已统一(其余 369 个组件本来就是对的)。

    a6249c8
  2. v0.21.0

    @hulianui/ui新功能

    三件「照文档写就是错的」:Navbar 居中段真的居中、极坐标图例可关、TreeSelect 选得到中间层

    三个 issue 的共同点是没有报错:写法照着文档,结果不对,肉眼容易当成自己写错了。

    Navbar:`NavbarBrand` 默认可伸长(默认行为变更)#81

    NavbarContent justify="center" 此前并不在导航栏中心。根因是三段伸缩性不对称:NavbarBrandshrink-0,两个 NavbarContentflex-1 平分剩余空间,于是居中段只居中在「自己那一格」里,整体随品牌名长度左偏(1440 宽、100px 品牌名实测偏左 265px;品牌名越长偏得越多,同一份代码在不同租户站点上偏移还不一样)。

    NavbarBrand 改为默认 flex-1 basis-0,三段等分。品牌内容仍靠 justify-start 贴左,且 flex 项默认 min-width: auto 不会被压小,brand 段与 end 段的视觉不变,变的只有中段真的落到了中心。

    有一种版式会因此改变:品牌 + 一段紧贴品牌的 `justify="start"` 内容(没有居中段)。等分后那段内容会被推到 1/3 处。这种版式传 grow={false} 回到旧行为:

    tsx
    <Navbar>
      <NavbarBrand grow={false}>瑚琏</NavbarBrand>
      <NavbarContent justify="start">…</NavbarContent> {/* 仍紧贴品牌 */}
    </Navbar>

    品牌区要在窄屏截断时,除 truncate 外仍需自行加 min-w-0(解开 flex 项的 min-width: auto),这点没变。

    Chart:`RadarChart` / `PieChart` / `RadialChart` 补 `legend`,六件全部补 `legendScroll`#80

    0.19.0 给 Area/Bar/Line 补了 legend 后,极坐标三件没跟上:它们的 <Legend> 写死在图内,消费方既关不掉也挪不动,自绘就变成两份图例并排(legendStyle 是内部常量,className 只到外层 div)。28 条序列时图例铺满 5 行,吃掉 height={320} 的一半有余,雷达盘被压扁、图例文字盖住角轴标签。

    现在三件都吃 legend?: boolean | "top" | "bottom",签名与笛卡尔三件一致。默认 `true`(它们历来自带图例),既有调用零改动;legend={false} 关掉。注意这是库内唯一一处默认值按图种分档的 prop:笛卡尔三件默认 false、极坐标三件默认 true

    代价说清楚:这三件的图例不再是 recharts 的 <Legend>,而是与其它三件同一套自绘图例(Dot 色点 + token 字号),色块从方形变圆点、间距字号略有差异;同时它不再参与 recharts 的内部高度分配,改由 height 精确让出一行。色点颜色与扇区/序列走同一条解析路径,不会对不上。

    另补 legendScroll(六件通用,默认 false):图例恒为单行 + 横向滚动,对齐 echarts 的 legend.type: "scroll"。序列多到换行时,「把 height 调大」并不成立——28 条序列的图例是 5 行,要把雷达盘撑回可读尺寸得把总高翻倍。开了它图例永远只占一行(让出 32px 给常显细滚动条),画布拿走其余全部:

    tsx
    {
      /* 关掉自带图例,自己画 */
    }
    <RadarChart legend={false} data={data} series={series} xKey="indicator" height={320} />;
    
    {
      /* 28 条序列:图例单行横滚,不吃画布 */
    }
    <RadarChart legendScroll data={data} series={series28} xKey="indicator" height={320} />;

    超出部分要横滑才看得到——序列多到几十条时这是取舍,不是免费的。

    TreeSelect:透传 `expandTrigger`,单选可以选到中间层#78

    单选 TreeSelect 此前只有叶子节点选得中:内部 TreeexpandTrigger 默认 "row",有子节点的行点了只展开就 return,走不到 setSelectedonChange 永远不触发,点几次都选不中,而这个能力没有开放给消费方。

    TreeSelect 现在透传 expandTrigger?: "row" | "icon",默认仍是 "row"(既有行为不变)。要「选到中间层」——选到某个部门、某个大类、某一册教材——传 "icon":箭头管展开、行的其余部分管选中,与多选态「勾选框管选、行管展开」在心智上对称。

    tsx
    <TreeSelect nodes={NODES} expandTrigger="icon" value={v} onChange={setV} placeholder="选择章节" />

    多选(checkable)不受影响:勾选框是独立命中区。三件的「禁忌 / 坑」都已补上对应说明——这三条此前在文档里全看不出来。

    61b47ea
  3. v0.20.0

    @hulianui/ui新功能

    运行时性能首轮:Combobox 大集合虚拟化 + 19 个组件跳过无谓重渲染

    新建的内部扫描器(packages/hulian-scan,private 不发布)用 react-scan + Playwright 把全部 372 个公开组件场景跑了一遍 React Profiler,首轮拿到 125 条硬 finding(55 avoidable-render、41 cascade-fanout、16 long-task、13 dropped-frames)。本次发版是把其中在 packed 消费态下仍可复现的那部分修掉,每项都在 workspace 与仓库外 tarball 两种环境复测过。

    Combobox / Select / RemoteSelect:大集合自动虚拟化(默认行为变更)

    items 给到 100 项及以上时列表自动虚拟化,只渲染视口内的项(@tanstack/react-virtual,已是既有依赖,不新增包体)。千项候选的展开从「一次挂载上千个 <li>」变成「挂载二三十个」。Selectsearchable 皮肤与 RemoteSelect 的候选列表走同一条路径,同样自动生效——RemoteSelect 是远程分页累积,翻够页数后会切过去。

    代价要说清楚:行高按 32px 固定估算,不做逐项测量。默认 ComboboxItem / SelectItem 恰好是 32px,所以绝大多数用法无感;但如果你的选项是两行文案、带头像、或用 className 改了 padding/字号,那么在 ≥100 项时滚动条长度与项的落位会逐渐偏移——不报错,短列表也复现不出来,只有滚到列表中后段才看得出跳动。三个组件因此都补了 virtualized 逃生口,这种选项显式传 virtualized={false} 即可回到全量渲染:

    tsx
    {/* 单行项 → 什么都不用改,≥100 项自动虚拟化 */}
    <Combobox items={CITIES}>…</Combobox>
    
    {/* renderOption 渲染「姓名 + 邮箱」两行 → 行高 ≠ 32px,关掉 */}
    <RemoteSelect fetcher={searchUsers} virtualized={false} renderOption={…} />

    依赖「选项全在 DOM 里」的测试同理:虚拟化后 getAllByRole("option") 只拿得到视口内那几条,断言总数改用列表容器上的 data-hulian-virtual-count,或对该用例传 virtualized={false}

    19 个组件跳过稳定 props 的重渲染

    Button、Calendar、Cascader、Checkbox、CodeDiff、CodeReviewThread、ColorSwatchPicker、ContributionGraph、CountrySelect、DatePicker、DateTimePicker、Gantt、Glimpse、Markdown、PricingTable、QRCode、Scheduler、TimePicker、TreeSelect 接了 memo。判据是扫描证据而非手感:只有当浅比较能安全跳过时才加,函数/ReactNode/可变对象 props 的组件单独看证据,没有批量塞自定义深比较。对外行为与 DOM 不变。

    其余定点优化

    • Selectsearchable 皮肤下按 value 找候选从每项 find() 线性扫改为 Map 查表,选项多时 trigger 与列表的每次渲染都少一轮 O(n)。
    • CircularGallery:削掉每帧重复的几何计算与纹理编码。
    • GhostCursor:降低 shader 每帧开销。
    • React 18 兼容回填:SelectTriggerProps 改用 ComponentPropsWithoutRef + 显式 refSwipeAction 的 ref 写法同步调整——两处此前只在 React 19 的类型下成立。
    0d9fb08

    组件内置文案全面接入 ConfigProvider locale

    ConfigProviderlocaleenUS 字典此前就在,但只有一部分组件真的读它——余下的把中文写死在组件里。接了 <ConfigProvider locale={enUS}> 的英文项目因此会看到一半英文一半中文,而且没有任何报错提示哪些组件没跟上。

    这批把 130 个组件的内置文案(按钮标签、空态、占位、aria-label、日期与星期格式、单位与分隔符等)接进 locale 字典,字典本身扩了 1688 行。除了整体翻译,几处按语言而非按字符串处理的差异也一并做了:Scheduler 的星期与日期区间按 locale 格式化(Jun 1 – Jun 7 / 6月1日 – 6月7日),CountrySelect 的国家名与副标题由 locale 决定取中文还是英文。

    对既有项目没有行为变化:不传 locale 时全部沿用原中文,缺失字典段落时逐条回退到组件内置中文(老版本的部分字典也不会因为缺 key 而崩)。要英文只需:

    tsx
    import { ConfigProvider, enUS } from "@hulianui/ui";
    
    <ConfigProvider locale={enUS}>{children}</ConfigProvider>;

    文档站同步产出英文版:376 个组件各配一份 .en.md(随包发布,MCP 的 get_component_doc 会读到),区块与页面示例、changelog、llms.txt / registry.json 等 AI 分发产物也都出了英文版。

  4. v0.19.1

    @hulianui/ui修复

    nav-menu.md:消歧 semantics 那条坑位,并补一个站点主导航示例(closes #76

    0.19.0 加 semantics 时(#69),props 表写的是「站点主导航选 `list`」,而禁忌/坑那条写成了
    「站点主导航留在默认 tree 档,读屏用户是真的找不到那些链接」—— 后者本意是条件警告(若留在
    tree 就找不到),但中文里「留在」既可以是「保持」,也可以出现在省略了「如果」的条件小句里,
    而这句前面正好是一句祈使(「别随便选」),读者的语感会顺着读成祈使句,于是变成「请留在 tree」,
    与 props 表相反。

    代价不对称:#69 整条 issue 就是围绕「主导航该是 list 还是 tree」,读错就把刚修好的可达性问题
    原样留着,而且两档皮肤一模一样、不会有任何报错。所以:

    • 把条件补全(「如果留在默认 tree 档 → 读屏按『列出页面所有链接』一条都找不到 → 那种场景请显式传 semantics="list"」),并点明「看不出选错」这个前提。
    • 示例区此前没有一个semantics,照抄就会退回默认档。现在把「站点主导航」作为第一个示例(带 semantics="list" + render 接路由),并给原来的会话列表示例注明它为什么不需要(命令式选择、行是 <button>,不是链接导航;若会话项是真链接则同样要传)。

    改的是随包发布的组件文档(src/**/*.md 在 npm 包内,MCP 的 get_component_doc 本地模式直接读它),
    所以发 patch 让消费方的 agent 也拿到修正后的文案。组件实现未改动。

    67038ed
  5. v0.19.0

    @hulianui/ui新功能

    新增 AuthPanel,六处逃生口清掉消费方缺口(closes #67 #69 #70 #71 #72 #73)

    两个下游(hulian-admin 的分屏登录/注册页、cairn 的试卷标注)一次报来六条,共同点是查完文档后仍绕不过去:分屏认证页的渐变面板只能裸 <div> + inline style,后台登录页的字段外观只能 className 覆盖,主导航为了保住 link 语义只能手写 <Link> 行,图例色点只能裸 <span>,框选坐标只能在调用处包一层 floor/ceil——全是 conventions 明令禁止的「业务侧打补丁」。这批把它们收回库内。

    新组件

    • AuthPanel:分屏登录/注册/找回密码页左侧那块宣传面板(渐变底 + 品牌 + 标语 + 卖点 + 底部区)。它存在的理由不是省几行 flex,而是渐变此前没有正经的表达方式——Tailwind 工具类给不出 radial-gradient(125% 125% at 0% 0%, color-mix(in oklab, …), …) 这种带 token 混色的写法,而 guard 的 no-style-override 是 error 级,两条一撞只剩裸 <div> + inline style 一条路(官方 signup block 自己就是这么写的,本次已换掉)。四档配方 radial / linear / mesh / none 都以 --color-bg 打底做 color-mix暗色自动跟随,不必另写一套colorresolveTone,与 Brand.color / Dot.color / ChartSeries.color 同一条路径(#71)。

    ```tsx
    <div className="grid min-h-dvh xl:grid-cols-2">
    <AuthPanel
    brand={<Brand name="瀚云" />}
    title="把想法送上全球边缘"
    highlights={["免费开始", "从 git push 到全球边缘上线"]}
    className="hidden xl:flex"
    />
    <div className="grid place-items-center p-8">
    <LoginForm surface={false} /> {/ 左面板已承担视觉重量,右边再套卡就是卡中卡 /}
    </div>
    </div>
    ```

    能力增强

    • LoginFormfieldssurface:前者是两个主字段的外观槽label / placeholder / prefix / suffix / description / autoComplete),只覆盖外观,取值与校验仍由模板托管,所以换 label 不会把浏览器的账号/密码自动填充弄丢;后者关掉自带卡面时把边框 / 底色 / 阴影 / 内距四件一起关——只关三件会逼消费方再写 xl:p-0 补最后一刀,等于没关(#70)。
    • NavMenusemantics?: "tree" | "list"(默认 tree,既有消费方零改动)。#59render 逃生口虽然渲出了真 <a>,但行上的 role="treeitem" 会压过它的隐式 link role:中键新标签页 / 右键复制链接回来了,无障碍树里它仍是 treeitem,读屏最常用的「列出页面所有链接」一条主导航都列不出来。list 档不写 role(<a> 是 link、<button> 是 button),选中态改用 aria-current="page",键盘退回「Tab 逐项 + 原生激活」——站点主导航在 ARIA APG 里本就是 list + link,tree 留给文件树 / 大纲树(#69)。
    • Dotcolor?: string:走 resolveTone 接任意色。五档 tone 接不住图表序列色(默认取值就是 chart-1..6),而图例色点要的正是「跟序列同色」。与 tone 同传时 color 优先(#73)。
    • AreaChart / BarChart / LineChartlegend?: boolean | "top" | "bottom":多序列图不给图例,读者无从知道哪条线是哪条序列。内部复用 Dot + series.label,色点与序列色同源。height 仍是组件总高——开图例时画布相应变矮,不会把总高撑高(#73)。
    • RegionSelecterrorPlaceholder / onError:底图 404 / 403 / 跨域 / 网络失败时有出口,不再永久停在「载入图片…」。预读此前只挂 onload 不挂 onerror;现在预读与画布 <image> 共用同一失败态(中途鉴权过期只让 SVG 那次请求失败时同样有出口),缓存里的失败结果(completenaturalWidth 为 0)也进失败态,src 变化会复位。后端按需渲染的底图(页图还没推到当前环境、签名 URL 过期、权限不足)这不是边缘情况,是常态(#67)。

    行为变更

    • RegionSelectonChange 现在给整数坐标(新增 round?: "expand" | "nearest" | "none",默认 expand;另导出纯函数 roundBox)。此前给的是浮点,而组件自称的坐标系是「原图像素」——落库(list[int] 之类的列约束)、服务端裁图(PIL / OpenCV / sharp 的 crop 都要整数,各自的隐式取整方向还不一致,裁出来差一两像素且没人解释得清)、box === savedBox 这种「有没有改过」的判断,三处都吃不下浮点。

    默认选 expand(左上 floor、右下 ceil)而不是 nearest取整不缩小框,否则一个刚好拖够 minSide 的框会被收成 minSide - 1,人明明拖够了却存不上,症状是「拖了没反应」。minSide 的判定也相应移到取整之后,与最终出口一致。拖拽预览(onDrafting)仍是浮点,视觉更跟手。要亚像素传 round="none" 即回到旧行为;已在调用处自己包 floor/ceil 的可以删掉了(#72)。

    文档

    两条会静默失效、光看代码看不出来的坑写进了对应的 <slug>.md

    • <Dot style={{ color }} /> 改不动圆点颜色——圆点是背景色,color 管的是文字色。那样写编译通过、guard 只报 no-style-override、页面上一律灰点,写的人以为生效了。自定义颜色只走 color prop。
    • RegionSelect 的取整缺陷在 1:1 或整数倍缩放下完全测不出来(坐标本就落在整数上)。自己写测试请用除不尽的比例,库内用的是 756→396。

    nav-menu.md 里那句「render 让读屏按链接播报」按实现更正为需配 `semantics="list"`——消费方正是照着这句话选型的。

    126ace2

还有 31 个更早版本,切到“全部”查看。