Skip to content
Lenso UI

v3.0.0-beta.8

新增 3 个组件(Badge、Pagination、Table)、DateField 的多项改进,以及关键的 API / 样式修复。

2026 年 3 月 2 日

本次发布新增三个组件:Badge、Pagination 与 Table,并为 DateField 与 TimeField 提供了新的 InputContainer 组合 API。

⚠️ 破坏性变更:TextField 的 CSS 类已从 .text-field 重命名为 .textfield。

HeroUI v3 Beta 8

安装

升级到最新版本:

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