Changelog

更新日志

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

当前版本

v0.23.0
npmGitHub Releases
  1. v0.23.0

    @hulianui/ui新功能

    RadarChart 半径轴可关;修 Banner 长文案撑破容器、SearchForm 窄屏挤成一团、MathText 关系符间距与 ^\circ 不解析

    RadarChart 新增 `radiusAxis`#86

    半径轴的刻度数字此前无条件渲染、className 够不到,消费方关不掉。它画在数据区里而不是外面:刻度锚点沿一条水平半径从雷达盘中心排到边缘,每个数字还被 recharts 旋转 90° 竖排。序列一多、数据填得满,前几个刻度就整个落进数据多边形内部,既压住图形又难读。

    echarts 的 radar 里 axisLabel.show 默认就是 false,只画环线与角轴名 —— 这串数字是移植到 recharts 时被默认带出来的,消费方既没要它也关不掉。

    tsx
    // 只留环线与角轴名(= echarts radar 的默认形态)
    <RadarChart radiusAxis={false} data={data} series={series} xKey="indicator" legendScroll />

    默认仍是 true,存量版式零改动。同时导出 RadarChartProps 类型。

    Banner 长文案撑破容器、把 action 挤出屏幕

    文案节点写的是 <span className="truncate">,而 truncate 里的 overflow:hiddentext-overflow:ellipsis 对 inline 元素不生效,只剩 white-space:nowrap —— 文字既不换行又不被裁剪,于是横向撑破 flex 容器,把 action 按钮顶出可视区(窄屏尤其明显)。加 block 后省略号才真正生效。凡是 Banner 配长文案的地方都受此影响,不限于移动端。

    SearchForm 在窄屏挤成一团

    gridTemplateColumns 写死在 inline style 里且没有断点,390px 的手机上仍按 columns(默认 3)分列,每列约 120px,「标签 + 控件」压进去后字段与操作区互相叠在一起。inline style 优先级还压过工具类,消费方自己也覆盖不掉。

    改为列数走 CSS 变量、sm 以下强制单列。子项的 colSpan 一并压成 col-auto —— 单列网格里 span 2 不会被夹到 1,而是创建隐式列,反而更溢出。桌面表现不变。

    MathText 关系符两侧留白不对称

    A \Rightarrow B 渲染成 A ⇒B:命令名后的空格作为终止符被吃掉,左侧文本空格却保留。修法不是保留原文空格(那样间距取决于作者打没打空格),而是按 TeX 的符号类别在渲染层给对称留白 —— 新增 op 节点,分 relation(= ≠ ≤ ⇒ ∈ ⊥ …)与 binary(× ÷ ± ∪ …)两档,前缀记号(∠ △ ⊙ ∴ …)仍紧贴其修饰对象,∠ABC 不会被拆开。类别按字符登记而非命令名,\neq 与上游 OCR 直接给的 同等对待。±/ 的一元用法(±3(±3))会降级成紧贴。

    MathText `^\circ` / `_\alpha` 整条路径不被解析

    ^_ 的单 token 简写此前只认单个字符,不认命令:90^\circ 原样输出 90^\circ,而 90^{\circ} 正常。90^\circ 是 LaTeX 写度数最常见的形式(少打两个花括号),\circ 在初中题面频次统计里排第三 —— 题面上直接露出原始记号,正是本组件要消灭的东西。现在 90^\circ90°x^\alpha b 的上标只吃 \alphab 仍是正文),不认识的命令照旧原样保留不吞内容(x^\oiint)。

    ⚠️ `mathToPlain` 的输出有变化

    关系符两侧的空格现在统一被归一化掉,与既有的紧凑口径对齐:

    mathToPlain("A = B")            // 旧 "A = B"    → 新 "A=B"
    mathToPlain("3 \\times 4 = 12") // 旧 "3 ×4 = 12" → 新 "3×4=12"

    旧行为里 \times 左有空格右没有,本身就是不对称的;而 mathToPlain("x\\neq 0")"x≠0" 这条紧凑口径一直如此。若下游拿 mathToPlain 的输出做全文检索或文本比对,需要同步。DOM 渲染的留白只发生在渲染层,不进朴素文本。

    ButtonGroup 文档补一条成员等高的坑

    连排靠 -ml-px 把相邻边框叠在一起,这个拼接假定成员等高。而 Button 的尺寸档里 icon(36px)没有等高的文字档(文字档是 sm 32 / md 40 / lg 48),所以 size="icon" 与任何带文字的按钮混排都会错位 —— 典型是 −/数值/+ 步进器。要混排就用等高的一对:iconSm(32) 配 sm(32)。这一条看代码发现不了,三个按钮读起来很整齐,只有渲染出来才看得见中间那个高 4px。

  2. 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
  3. 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
  4. 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 分发产物也都出了英文版。

  5. 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

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