v3.0.0-beta.8
新增 3 个组件(Badge、Pagination、Table)、DateField 的多项改进,以及关键的 API / 样式修复。
本次发布新增三个组件:Badge、Pagination 与 Table,并为 DateField 与 TimeField 提供了新的 InputContainer 组合 API。
⚠️ 破坏性变更:TextField 的 CSS 类已从 .text-field 重命名为 .textfield。
安装
升级到最新版本:
npm i @lenso/tokens@beta @lenso/ui@beta正在使用 AI 助手? 只需对它说一句「Hey Cursor,把 HeroUI 升级到最新版本」,AI 助手就会自动对比版本并应用必要的变更。了解更多请参阅 HeroUI MCP 服务器。
新增功能
新组件
- Badge:紧凑的状态 + 计数指示器,可配置颜色、变体、放置位置与尺寸。(文档)
- Pagination:分页相关的复合组件基元,提供摘要、省略号以及上一页 / 下一页等控件。(文档)
- Table:数据表格基元,支持排序、选择、列宽调整、异步加载以及表脚组合。(文档)
Badge
新增徽章基元,可用于计数、标签以及通过 Badge.Anchor 与 Badge.Label 锚定的浮层。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// oxlint-disable jsx-a11y/prefer-tag-over-role -- The CSS-painted status dot has no image resource for an HTML img.// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Avatar, Badge } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { s } from "../card/display.stylex";
export function BadgeBasic() { return ( <div {...stylex.props(s.row6)}> <Badge.Anchor> <Avatar> <Avatar.Image alt="John Doe" src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg" /> <Avatar.Fallback>JD</Avatar.Fallback> </Avatar> <Badge color="danger" size="sm"> 5 </Badge> </Badge.Anchor> <Badge.Anchor> <Avatar> <Avatar.Image alt="Alex Brown" src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg" /> <Avatar.Fallback>AB</Avatar.Fallback> </Avatar> <Badge color="accent" size="sm"> New </Badge> </Badge.Anchor> <Badge.Anchor> <Avatar> <Avatar.Image alt="Chris Davis" src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg" /> <Avatar.Fallback>CD</Avatar.Fallback> </Avatar> <Badge role="img" aria-label="Online" color="success" placement="bottom-right" size="sm" /> </Badge.Anchor> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Pagination
新增导航组件,由可组合的部件构成(Root、Content、Item、Link、Previous、Next、Summary、Ellipsis)。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";export { PaginationWithEllipsis } from "./source";Local adaptation source above. Derived from HeroUI v3.2.6 source.
Table
基于 React Aria 构建的复合数据表格,支持可排序的列、行选择、自定义单元格、加载更多哨兵行以及可调整宽度的列。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 table-basic (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { Table } from "@lenso/ui";
const rows = [ { key: "kate", name: "Kate Moore", role: "CEO", status: "Active", email: "[email protected]" }, { key: "john", name: "John Smith", role: "CTO", status: "Active", email: "[email protected]" }, { key: "sara", name: "Sara Johnson", role: "CMO", status: "On Leave", email: "[email protected]" }, { key: "michael", name: "Michael Brown", role: "CFO", status: "Active", email: "[email protected]", },] as const;const styles = stylex.create({ content: { minWidth: 600 } });
export function Basic() { return ( <Table> <Table.ScrollContainer> <Table.Content aria-label="Team members" xstyle={styles.content}> <Table.Header> <Table.Column columnKey="name">Name</Table.Column> <Table.Column columnKey="role">Role</Table.Column> <Table.Column columnKey="status">Status</Table.Column> <Table.Column columnKey="email">Email</Table.Column> </Table.Header> <Table.Body> <Table.Collection items={rows}> {(row) => ( <Table.Row itemKey={row.key}> <Table.Cell>{row.name}</Table.Cell> <Table.Cell>{row.role}</Table.Cell> <Table.Cell>{row.status}</Table.Cell> <Table.Cell>{row.email}</Table.Cell> </Table.Row> )} </Table.Collection> </Table.Body> </Table.Content> </Table.ScrollContainer> </Table> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
自定义单元格:
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6 custom-cells adaptation (Apache-2.0).import { useState, type Key } from "react";import * as stylex from "@stylexjs/stylex";import { Icon } from "@iconify/react";import { Avatar, Button, Table, type SortDescriptor } from "@lenso/ui";import { Status, styles, users, type User } from "./data";const workers = users.slice(0, 5).map((user, index) => ({ ...user, key: [4586932, 5273849, 7492836, 8293746, 1234567][index]!, role: [ "Chief Executive Officer", "Chief Technology Officer", "Chief Marketing Officer", "Chief Financial Officer", "Product Manager", ][index]!, color: ["red", "green", "blue", "purple", "orange"][index],}));export function CustomCells() { const [selected, setSelected] = useState<Set<Key>>(new Set()); const [sort, setSort] = useState<SortDescriptor>({ column: "name", direction: "ascending" }); const sorted = [...workers].sort( (a, b) => String(a[sort.column as keyof User]).localeCompare(String(b[sort.column as keyof User])) * (sort.direction === "ascending" ? 1 : -1), ); return ( <Table> <Table.ScrollContainer> <Table.Content aria-label="Table with custom cells" xstyle={styles.customCells} selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} sortDescriptor={sort} onSortChange={setSort} > <Table.Header> <Table.Column columnKey="selection" width={48}> <Table.SelectionCheckbox /> </Table.Column> {[ ["key", "Worker ID"], ["name", "Member"], ["role", "Role"], ["status", "Status"], ].map(([key, title]) => ( <Table.Column key={key} columnKey={key!} allowsSorting> {({ sortDirection }) => ( <Table.SortableColumnHeader sortDirection={sortDirection}> {title} </Table.SortableColumnHeader> )} </Table.Column> ))} <Table.Column columnKey="actions">Actions</Table.Column> </Table.Header> <Table.Body> {sorted.map((user) => ( <Table.Row key={user.key} itemKey={user.key}> <Table.Cell> <Table.SelectionCheckbox aria-label={`Select ${user.name}`} /> </Table.Cell> <Table.Cell> <div {...stylex.props(styles.row)}> #{user.key} <Button isIconOnly aria-label={`Copy ID #${user.key}`} size="sm" variant="ghost" > <Icon icon="gravity-ui:copy" width={16} aria-hidden="true" /> </Button> </div> </Table.Cell> <Table.Cell> <div {...stylex.props(styles.row)}> <Avatar size="sm"> <Avatar.Image alt={user.name} src={`https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/${user.color}.jpg`} /> <Avatar.Fallback> {user.name .split(" ") .map((name) => name[0]) .join("")} </Avatar.Fallback> </Avatar> <div {...stylex.props(styles.detail)}> <span>{user.name}</span> <span {...stylex.props(styles.muted)}>{user.email}</span> </div> </div> </Table.Cell> <Table.Cell>{user.role}</Table.Cell> <Table.Cell> <Status status={user.status} /> </Table.Cell> <Table.Cell> <div {...stylex.props(styles.row)}> {(["View", "Edit", "Delete"] as const).map((action, index) => ( <Button key={action} isIconOnly aria-label={`${action} ${user.name}`} size="sm" variant={action === "Delete" ? "danger-soft" : "tertiary"} > <Icon icon={`gravity-ui:${["eye", "pencil", "trash-bin"][index]}`} width={16} aria-hidden="true" /> </Button> ))} </div> </Table.Cell> </Table.Row> ))} </Table.Body> </Table.Content> </Table.ScrollContainer> </Table> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
分页:
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6 pagination adaptation (Apache-2.0).import { useState } from "react";import { Table } from "@lenso/ui";import { Headers, Pages, Rows, styles, users } from "./data";export function PaginationDemo() { const [page, setPage] = useState(1); return ( <Table> <Table.ScrollContainer> <Table.Content aria-label="Table with pagination" xstyle={styles.content}> <Headers /> <Table.Body> <Rows items={users.slice((page - 1) * 4, page * 4)} /> </Table.Body> </Table.Content> </Table.ScrollContainer> <Table.Footer> <Pages page={page} count={2} total={8} setPage={setPage} /> </Table.Footer> </Table> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
空状态:
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6 empty-state adaptation (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { Icon } from "@iconify/react";import { EmptyState, Table } from "@lenso/ui";import { Headers, styles } from "./data";const empty = stylex.create({ root: { minHeight: 200 }, content: { minHeight: 140, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 16, textAlign: "center", color: "var(--muted)", },});export function EmptyStateDemo() { return ( <Table xstyle={empty.root}> <Table.ScrollContainer> <Table.Content aria-label="Empty table" xstyle={styles.content}> <Headers /> <Table.Body> <tr> <td colSpan={4}> <EmptyState xstyle={empty.content}> <Icon icon="gravity-ui:tray" width={24} aria-hidden="true" /> <span>No results found</span> </EmptyState> </td> </tr> </Table.Body> </Table.Content> </Table.ScrollContainer> </Table> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
组件 + API 改进
DateField 与 TimeField 的增强
DateField 与 TimeField 现在暴露了 InputContainer,用于在前缀与后缀内容之间包裹输入段(segment)。
之前:
<DateField.Group> <DateField.Prefix>...</DateField.Prefix> <DateField.Input> {(segment) => <DateField.Segment segment={segment} />} </DateField.Input> <DateField.Suffix>...</DateField.Suffix></DateField.Group>之后:
<DateField.Group> <DateField.Prefix>...</DateField.Prefix> <DateField.InputContainer> <DateField.Input> {(segment) => <DateField.Segment segment={segment} />} </DateField.Input> </DateField.InputContainer> <DateField.Suffix>...</DateField.Suffix></DateField.Group>此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";/** Adapted from HeroUI v3.2.6 release-input-container.json. Copyright NextUI Inc. Apache-2.0. */import { DateRangePicker, TimeField } from "@lenso/ui";import { getLocalTimeZone, parseZonedDateTime } from "@internationalized/date";import * as stylex from "@stylexjs/stylex";import { formatStyles } from "../date-picker/format-controls";import { PickerCalendar, RangeInput } from "./parts";
export function InputContainer() { const localTimeZone = getLocalTimeZone(); const defaultValue = { start: parseZonedDateTime(`2026-02-03T08:45:00[${localTimeZone}]`), end: parseZonedDateTime(`2026-02-10T18:45:00[${localTimeZone}]`), }; return ( <DateRangePicker shouldForceLeadingZeros xstyle={formatStyles.releaseField} defaultValue={defaultValue} granularity="second" hourCycle={12} > {({ state }) => ( <> <DateRangePicker.Label>Date range</DateRangePicker.Label> <RangeInput container /> <DateRangePicker.Popover xstyle={formatStyles.releasePopover}> <PickerCalendar /> <div {...stylex.props(formatStyles.times)}> <div {...stylex.props(formatStyles.timeRow)}> <span {...stylex.props(formatStyles.label)}>Start Time</span> <TimeField shouldForceLeadingZeros aria-label="Start Time" granularity="second" hourCycle={12} value={state.timeRange?.start ?? null} onChange={(value) => state.setTime("start", value)} > <TimeField.Group variant="secondary"> <TimeField.Input> {(segment) => <TimeField.Segment segment={segment} />} </TimeField.Input> </TimeField.Group> </TimeField> </div> <div {...stylex.props(formatStyles.timeRow)}> <span {...stylex.props(formatStyles.label)}>End Time</span> <TimeField shouldForceLeadingZeros aria-label="End Time" granularity="second" hourCycle={12} value={state.timeRange?.end ?? null} onChange={(value) => state.setTime("end", value)} > <TimeField.Group variant="secondary"> <TimeField.Input> {(segment) => <TimeField.Segment segment={segment} />} </TimeField.Input> </TimeField.Group> </TimeField> </div> </div> </DateRangePicker.Popover> </> )} </DateRangePicker> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
⚠️ 破坏性变更
TextField 类名 + 路径重命名
为避免与 Tailwind 的 text-* 工具类前缀冲突,TextField 的样式命名已统一调整。
| 组件 | 旧类名 | 新类名 | 说明 |
|---|---|---|---|
| TextField Root | .text-field | .textfield | 根类名重命名 |
| TextField Full Width | .text-field--full-width | .textfield--full-width | 修饰类重命名 |
同一变更涉及的其他重命名:
- 样式文件:
text-field.css->textfield.css - 样式导出路径:
@lenso/tokens/src/components/text-field->@lenso/tokens/src/components/textfield
样式修复
- RangeCalendar:为日历单元格添加圆角,优化范围选择的视觉效果(#6270)
Bug 修复
- 通过将
isRequired转化为data-required,为 DatePicker 与 DateRangePicker 补齐了必填状态的红色星号行为(#6270) - 修复了 Autocomplete 与 Select 中触发器无效状态样式缺失的问题——将 invalid 样式限定在根状态范围内(#6270)
- 更新了 TextField 的文档与演示引用,使其指向新的
textfield-*demo key 以及对应的源码 / 样式路径(#6270)
链接
贡献者
感谢每一位为本次发布做出贡献的开发者!
HeroUI contributors