Skip to content
Lenso UI

v3.0.0-beta.7

新增 4 个组件(Calendar、RangeCalendar、DatePicker、DateRangePicker)以及多项 API 改进。

2026 年 2 月 19 日

本次发布新增 4 个组件:Calendar、RangeCalendar、DatePicker 与 DateRangePicker。同时还引入了 Switch.Content,用于将 label 与 description 组合到 Switch 控件旁边;以及 Tabs.Separator,用于在 Tab 之间按需添加分隔线。

⚠️ 破坏性变更:从 Tabs 中移除了 hideSeparator;DateInputGroup 与 ColorInputGroup 已分别合并到 DateField.Group、TimeField.Group 与 ColorField.Group 之下。

HeroUI v3 Beta 7

安装

升级到最新版本:

npm i @lenso/tokens@beta @lenso/ui@beta

正在使用 AI 助手? 只需对它说一句「Hey Cursor,把 HeroUI 升级到最新版本」,AI 助手就会自动对比版本并应用必要的变更。了解更多请参阅 HeroUI MCP 服务器。

新增功能

日期与时间体系

日期与时间 —— Calendar、DatePicker、RangeCalendar 与 DateRangePicker 均基于 React Aria 的日期基元构建。支持国际化、时区,以及完整的键盘导航与 ARIA 无障碍能力。

主要特性:

  • 历法系统:公历、佛历、波斯历等
  • 年份选择器:用于快速跳转年份的浮层
  • 单元格指示器:在单元格上展示事件、可用状态或状态点
  • 范围选择:日期范围带有视觉高亮
  • 无障碍:键盘导航、屏幕阅读器、ARIA 全部支持

所有日期值都使用 @internationalized/date 提供的类型(CalendarDate、CalendarDateTime、ZonedDateTime)。可以用 I18nProvider 覆盖区域设置,并通过 useLocale 读取它。

新组件

Calendar

支持单日期选择的日历,包含年份选择器、单元格指示器、多月份视图以及国际化历法。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";
/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { Calendar } from "@lenso/ui";
export function Basic() {  return (    <Calendar aria-label="Event date">      <Calendar.Header>        <Calendar.Heading />        <Calendar.NavButton slot="previous" />        <Calendar.NavButton slot="next" />      </Calendar.Header>      <Calendar.Grid>        <Calendar.GridHeader>          {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}        </Calendar.GridHeader>        <Calendar.GridBody>{(date) => <Calendar.Cell date={date} />}</Calendar.GridBody>      </Calendar.Grid>    </Calendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

年份选择器:

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { Calendar } from "@lenso/ui";
export function YearPicker() {  return (    <Calendar aria-label="Event date">      <Calendar.Header>        <Calendar.YearPickerTrigger>          <Calendar.YearPickerTriggerHeading />          <Calendar.YearPickerTriggerIndicator />        </Calendar.YearPickerTrigger>        <Calendar.NavButton slot="previous" />        <Calendar.NavButton slot="next" />      </Calendar.Header>      <Calendar.Grid>        <Calendar.GridHeader>          {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}        </Calendar.GridHeader>        <Calendar.GridBody>{(date) => <Calendar.Cell date={date} />}</Calendar.GridBody>      </Calendar.Grid>      <Calendar.YearPickerGrid>        <Calendar.YearPickerGridBody>          {({ year }) => <Calendar.YearPickerCell year={year} />}        </Calendar.YearPickerGridBody>      </Calendar.YearPickerGrid>    </Calendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

国际化历法:

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { Calendar } from "@lenso/ui";import { getLocalTimeZone, today } from "@internationalized/date";import { I18nProvider } from "react-aria-components/I18nProvider";
export function InternationalCalendar() {  return (    <I18nProvider locale="hi-IN-u-ca-indian">      <Calendar aria-label="Event date" defaultValue={today(getLocalTimeZone())}>        <Calendar.Header>          <Calendar.YearPickerTrigger>            <Calendar.YearPickerTriggerHeading />            <Calendar.YearPickerTriggerIndicator />          </Calendar.YearPickerTrigger>          <Calendar.NavButton slot="previous" />          <Calendar.NavButton slot="next" />        </Calendar.Header>        <Calendar.Grid>          <Calendar.GridHeader>            {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}          </Calendar.GridHeader>          <Calendar.GridBody>{(date) => <Calendar.Cell date={date} />}</Calendar.GridBody>        </Calendar.Grid>        <Calendar.YearPickerGrid>          <Calendar.YearPickerGridBody>            {({ year }) => <Calendar.YearPickerCell year={year} />}          </Calendar.YearPickerGridBody>        </Calendar.YearPickerGrid>      </Calendar>    </I18nProvider>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

RangeCalendar

日期范围选择,支持范围高亮与多月份视图。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";
/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { RangeCalendar } from "@lenso/ui";
export function Basic() {  return (    <RangeCalendar aria-label="Trip dates" firstDayOfWeek="mon">      <RangeCalendar.Header>        <RangeCalendar.Heading />        <RangeCalendar.NavButton slot="previous" />        <RangeCalendar.NavButton slot="next" />      </RangeCalendar.Header>      <RangeCalendar.Grid>        <RangeCalendar.GridHeader>          {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}        </RangeCalendar.GridHeader>        <RangeCalendar.GridBody>          {(date) => <RangeCalendar.Cell date={date} />}        </RangeCalendar.GridBody>      </RangeCalendar.Grid>    </RangeCalendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

多月份显示:

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  root: { containerType: "normal", width: "100%", maxWidth: "none", overflowX: "auto" },  months: { marginInline: "auto", display: "flex", width: "max-content", gap: 32 },  month: { width: 256 },  heading: { flex: "none" },  spacer: { width: 24, height: 24 },});export function MultipleMonths() {  return (    <RangeCalendar aria-label="Trip dates" xstyle={styles.root} visibleDuration={{ months: 2 }}>      <div {...stylex.props(styles.months)}>        <div {...stylex.props(styles.month)}>          <RangeCalendar.Header>            <RangeCalendar.NavButton slot="previous" />            <RangeCalendar.Heading xstyle={styles.heading} />            <div {...stylex.props(styles.spacer)} />          </RangeCalendar.Header>          <RangeCalendar.Grid>            <RangeCalendar.GridHeader>              {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}            </RangeCalendar.GridHeader>            <RangeCalendar.GridBody>              {(date) => <RangeCalendar.Cell date={date} />}            </RangeCalendar.GridBody>          </RangeCalendar.Grid>        </div>        <div {...stylex.props(styles.month)}>          <RangeCalendar.Header>            <div {...stylex.props(styles.spacer)} />            <RangeCalendar.Heading xstyle={styles.heading} offset={{ months: 1 }} />            <RangeCalendar.NavButton slot="next" />          </RangeCalendar.Header>          <RangeCalendar.Grid offset={{ months: 1 }}>            <RangeCalendar.GridHeader>              {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}            </RangeCalendar.GridHeader>            <RangeCalendar.GridBody>              {(date) => <RangeCalendar.Cell date={date} />}            </RangeCalendar.GridBody>          </RangeCalendar.Grid>        </div>      </div>    </RangeCalendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

DatePicker

日期输入框 + popover 日历。支持格式选项、国际化、自定义指示器与表单校验。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";
/** Adapted from the pinned HeroUI v3.2.6 basic.json. Copyright NextUI Inc. Apache-2.0. Local RAC label and StyleX replace source Label and utility classes. */import { Calendar, DateField, DatePicker } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({ field: { width: 288 } });export function Basic() {  return (    <DatePicker xstyle={styles.field} name="date">      <DatePicker.Label>Date</DatePicker.Label>      <DateField.Group fullWidth>        <DateField.Input>{(segment) => <DateField.Segment segment={segment} />}</DateField.Input>        <DateField.Suffix>          <DatePicker.Trigger>            <DatePicker.TriggerIndicator />          </DatePicker.Trigger>        </DateField.Suffix>      </DateField.Group>      <DatePicker.Popover>        <Calendar aria-label="Event date">          <Calendar.Header>            <Calendar.YearPickerTrigger>              <Calendar.YearPickerTriggerHeading />              <Calendar.YearPickerTriggerIndicator />            </Calendar.YearPickerTrigger>            <Calendar.NavButton slot="previous" />            <Calendar.NavButton slot="next" />          </Calendar.Header>          <Calendar.Grid>            <Calendar.GridHeader>              {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}            </Calendar.GridHeader>            <Calendar.GridBody>{(date) => <Calendar.Cell date={date} />}</Calendar.GridBody>          </Calendar.Grid>          <Calendar.YearPickerGrid>            <Calendar.YearPickerGridBody>              {({ year }) => <Calendar.YearPickerCell year={year} />}            </Calendar.YearPickerGridBody>          </Calendar.YearPickerGrid>        </Calendar>      </DatePicker.Popover>    </DatePicker>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

日期与时间(搭配 TimeField):

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** Adapted from HeroUI v3.2.6 format-options.json. Copyright NextUI Inc. Apache-2.0. */import { DatePicker, TimeField } from "@lenso/ui";import {  getLocalTimeZone,  parseDate,  parseZonedDateTime,  type DateValue,} from "@internationalized/date";import { useMemo, useState } from "react";import * as stylex from "@stylexjs/stylex";import { PickerCalendar, PickerInput } from "./parts";import { FormatControls, formatStyles, type Granularity, type HourCycle } from "./format-controls";
export function FormatOptions() {  const [granularity, setGranularity] = useState<Granularity>("minute");  const [hourCycle, setHourCycle] = useState<HourCycle>(12);  const [hideTimeZone, setHideTimeZone] = useState(false);  const [shouldForceLeadingZeros, setShouldForceLeadingZeros] = useState(false);  const timeGranularity = granularity !== "day" ? granularity : undefined;  const defaultValue = useMemo<DateValue>(    () =>      granularity === "day"        ? parseDate("2026-02-03")        : parseZonedDateTime(`2026-02-03T08:45:00[${getLocalTimeZone()}]`),    [granularity],  );  return (    <div {...stylex.props(formatStyles.stack)}>      <DatePicker        key={granularity}        xstyle={formatStyles.field}        defaultValue={defaultValue}        granularity={granularity}        hourCycle={hourCycle}        hideTimeZone={hideTimeZone}        shouldForceLeadingZeros={shouldForceLeadingZeros}        name="date"      >        {({ state }) => (          <>            <DatePicker.Label>Date and time</DatePicker.Label>            <PickerInput />            <DatePicker.Popover xstyle={formatStyles.popover}>              <PickerCalendar />              {timeGranularity && (                <div {...stylex.props(formatStyles.timeRow)}>                  <span {...stylex.props(formatStyles.label)}>Time</span>                  <TimeField                    aria-label="Time"                    granularity={timeGranularity}                    hourCycle={hourCycle}                    hideTimeZone={hideTimeZone}                    name="time"                    shouldForceLeadingZeros={shouldForceLeadingZeros}                    value={state.timeValue}                    onChange={(value) => {                      if (value) state.setTimeValue(value);                    }}                  >                    <TimeField.Group variant="secondary">                      <TimeField.Input>                        {(segment) => <TimeField.Segment segment={segment} />}                      </TimeField.Input>                    </TimeField.Group>                  </TimeField>                </div>              )}            </DatePicker.Popover>          </>        )}      </DatePicker>      <FormatControls        {...{          granularity,          setGranularity,          hourCycle,          setHourCycle,          hideTimeZone,          setHideTimeZone,          shouldForceLeadingZeros,          setShouldForceLeadingZeros,        }}      />    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

DateRangePicker

两个日期输入框 + popover 范围日历。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";
/** Adapted from the pinned HeroUI v3.2.6 basic.json. Copyright NextUI Inc. Apache-2.0. Local RAC label and StyleX replace source Label and utility classes. */import { DateField, DateRangePicker, RangeCalendar } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({ field: { width: 320 } });export function Basic() {  return (    <DateRangePicker xstyle={styles.field} endName="endDate" startName="startDate">      <DateRangePicker.Label>Trip dates</DateRangePicker.Label>      <DateField.Group fullWidth>        <DateField.Input slot="start">          {(segment) => <DateField.Segment segment={segment} />}        </DateField.Input>        <DateRangePicker.RangeSeparator />        <DateField.Input slot="end">          {(segment) => <DateField.Segment segment={segment} />}        </DateField.Input>        <DateField.Suffix>          <DateRangePicker.Trigger>            <DateRangePicker.TriggerIndicator />          </DateRangePicker.Trigger>        </DateField.Suffix>      </DateField.Group>      <DateRangePicker.Popover>        <RangeCalendar aria-label="Trip dates">          <RangeCalendar.Header>            <RangeCalendar.YearPickerTrigger>              <RangeCalendar.YearPickerTriggerHeading />              <RangeCalendar.YearPickerTriggerIndicator />            </RangeCalendar.YearPickerTrigger>            <RangeCalendar.NavButton slot="previous" />            <RangeCalendar.NavButton slot="next" />          </RangeCalendar.Header>          <RangeCalendar.Grid>            <RangeCalendar.GridHeader>              {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}            </RangeCalendar.GridHeader>            <RangeCalendar.GridBody>              {(date) => <RangeCalendar.Cell date={date} />}            </RangeCalendar.GridBody>          </RangeCalendar.Grid>          <RangeCalendar.YearPickerGrid>            <RangeCalendar.YearPickerGridBody>              {({ year }) => <RangeCalendar.YearPickerCell year={year} />}            </RangeCalendar.YearPickerGridBody>          </RangeCalendar.YearPickerGrid>        </RangeCalendar>      </DateRangePicker.Popover>    </DateRangePicker>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

API 改进

Switch.Content

Switch.Content 用于将 label 与 description 组合到 Switch 控件旁边(#6240)。

之前:

import { Switch, Label, Description } from '@lenso/ui';
<Switch>  <Switch.Control>    <Switch.Thumb />  </Switch.Control>  <Switch.Content>    <Label>Email notifications</Label>    <Description>Get notified when someone mentions you</Description>  </Switch.Content></Switch>

Tabs.Separator

Tabs 组件现在新增了一个显式的 Tabs.Separator 子组件,用于在 Tab 之间添加视觉分隔线。它取代了之前自动生成的 CSS 伪元素分隔线以及 hideSeparator prop(#6243)。

分隔线现在改为 按需启用 —— 在希望出现分隔线的 <Tabs.Tab> 内部添加 <Tabs.Separator /> 即可。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";export { WithSeparator } from "./source";

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Field 子组件的合并

DateField、TimeField 与 ColorField 现在直接暴露各自的输入组子组件,不再需要单独引入 DateInputGroup 或 ColorInputGroup。具体的迁移方式请参阅 破坏性变更。

传给 Breadcrumbs.Item 的 props 现在会正确转发到底层的 Link(#6233)。

样式修复

  • ListBox Item:将悬停背景色从 bg-default-hover 调整为 bg-default,以保持一致性
  • Date Input Group:将段(segment)文本从 tabular-nums 调整为 text-nowrap,优化布局
  • Date Input Group:改进 focus-within 样式,使其将日期选择器触发器排除在字段聚焦高亮之外

依赖更新

  • React Aria Components:从 1.14.0 升级到 1.15.0 —— 新增了 render prop,可用于自定义任何 React Aria 组件渲染的 DOM 元素(适用于路由链接、Motion 等动画库)
  • @react-aria/utils:从 3.32.0 升级到 3.33.0
  • @react-types/shared:从 3.32.1 升级到 3.33.0
  • @internationalized/date:从 3.10.1 升级到 3.11.0 —— 日期字段现在改为在失焦时进行约束,而不是在输入过程中实时约束
  • 新增 @react-aria/i18n 与 @react-stately/utils,用于日历的国际化

⚠️ 破坏性变更

Tabs —— 移除 hideSeparator prop

hideSeparator prop 已从 Tabs 组件中移除。分隔线现在改为 按需启用,通过新增的 <Tabs.Separator /> 子组件来添加,而不再通过 CSS 伪元素自动生成(#6243)。

之前:

{/* Separators shown by default, hidden via prop */}<Tabs hideSeparator>  <Tabs.List>    <Tabs.Tab id="tab1">Tab 1<Tabs.Indicator /></Tabs.Tab>    <Tabs.Tab id="tab2">Tab 2<Tabs.Indicator /></Tabs.Tab>  </Tabs.List></Tabs>

之后:

{/* No separators by default — explicitly add them where needed */}<Tabs>  <Tabs.List>    <Tabs.Tab id="tab1"><Tabs.Indicator />Tab 1</Tabs.Tab>    <Tabs.Tab id="tab2"><Tabs.Separator /><Tabs.Indicator />Tab 2</Tabs.Tab>  </Tabs.List></Tabs>

CSS 变更:

  • Tab 的分隔线样式已从伪元素(.tabs__tab:not(:first-child):before)迁移到独立的 .tabs__separator 类
  • 已移除 [data-hide-separator] 这一 data 属性

Field 子组件 API 变更

DateInputGroup 与 ColorInputGroup 不再从 @lenso/ui 直接导出。它们的子组件已分别合并到对应的 Field 组件之下(DateField、TimeField、ColorField)。

DateField 变更

之前:

import {DateField, Label, DateInputGroup, Description} from '@lenso/ui';
<DateField>  <Label>Date</Label>  <DateInputGroup>    <DateInputGroup.Prefix>...</DateInputGroup.Prefix>    <DateInputGroup.Input>      {(segment) => <DateInputGroup.Segment segment={segment} />}    </DateInputGroup.Input>    <DateInputGroup.Suffix>...</DateInputGroup.Suffix>  </DateInputGroup>  <Description>Pick a date</Description></DateField>

之后:

import {DateField, Label, Description} from '@lenso/ui';
<DateField>  <Label>Date</Label>  <DateField.Group>    <DateField.Prefix>...</DateField.Prefix>    <DateField.Input>      {(segment) => <DateField.Segment segment={segment} />}    </DateField.Input>    <DateField.Suffix>...</DateField.Suffix>  </DateField.Group>  <Description>Pick a date</Description></DateField>

TimeField 变更

模式与 DateField 相同:

之前之后
DateInputGroupTimeField.Group
DateInputGroup.InputTimeField.Input
DateInputGroup.SegmentTimeField.Segment
DateInputGroup.PrefixTimeField.Prefix
DateInputGroup.SuffixTimeField.Suffix

ColorField 变更

之前之后
ColorInputGroupColorField.Group
ColorInputGroup.InputColorField.Input
ColorInputGroup.PrefixColorField.Prefix
ColorInputGroup.SuffixColorField.Suffix

用法:

import {ColorField, Label, ColorInputGroup, ColorSwatch} from '@lenso/ui';
<ColorField>  <Label>Color</Label>  <ColorInputGroup>    <ColorInputGroup.Prefix>      <ColorSwatch color="#000" />    </ColorInputGroup.Prefix>    <ColorInputGroup.Input />  </ColorInputGroup></ColorField>

之后:

import {ColorField, Label, ColorSwatch} from '@lenso/ui';
<ColorField>  <Label>Color</Label>  <ColorField.Group>    <ColorField.Prefix>      <ColorSwatch color="#000" />    </ColorField.Prefix>    <ColorField.Input />  </ColorField.Group></ColorField>

说明: 底层的 CSS 类名(.date-input-group、.color-input-group 等)保持不变,仅 JavaScript 引入路径与组件名称发生了变化。

链接

贡献者

感谢每一位为本次发布做出贡献的开发者!

HeroUI contributors