跳到主要内容

Tree

  • 组件说明:Folders 风格的树形导航 Pattern(文件夹 / 分组列表场景)。API 习惯参考 antd Tree。文档归类于 Patterns;实现位于 packages/design/src/components/Tree。
  • 交互特征:点击任意未禁用行会选中。展开 / 折叠仅由左侧 chevron 触发(chevron 在 row hover / focus-within 时淡入显示)。键盘:Enter / Space 选中;ArrowLeft / ArrowRight 折叠 / 展开。可选 HTML5 拖拽(before / inside / after 三档、防成环、禁用行不可作为投放目标、折叠 folder 悬停自动展开)。
  • 实现约定:Tree 视觉由 packages/design/src/components/Tree 直接接管,没有 ui/ primitive,样式全部基于 design token 组合。
  • Figma 规范

基础用法​

结果
Loading...
实时编辑器
<Tree
  defaultExpandedKeys={['projects']}
  treeData={[
    { key: 'all', title: 'All Recordings' },
    {
      key: 'projects',
      title: 'Projects',
      children: [
        { key: 'projects/web', title: 'Web Refactor' },
        { key: 'projects/mobile', title: 'Mobile App' },
      ],
    },
    { key: 'starred', title: 'Starred' },
    { key: 'trash', title: 'Trash', disabled: true },
  ]}
/>

Tree 不再提供单独的"计数 / 尾部信息"插槽——把计数等附属内容直接拼到 title 里(例如 <span>Projects <span className="text-(--Labels-Tertiary)">(99)</span></span>)。

状态​

四种交互状态:Default / Hover / Selected / Disabled。按 Figma 规范,Selected 默认与 hover 共用背景色(--Grays-Gray-1);需要区分选中色时传入 selectedColor。

结果
Loading...
实时编辑器
<Tree
  defaultSelectedKeys={['selected']}
  treeData={[
    { key: 'default', title: '默认项 — 鼠标悬浮试试' },
    { key: 'selected', title: '选中项' },
    { key: 'disabled', title: '禁用项', disabled: true },
  ]}
/>

自定义 selected 颜色​

Tree 的 selectedColor 可覆盖选中行背景。接受任意 CSS color 或 var() 表达式,通常传 design token:

结果
Loading...
实时编辑器
<Tree
  defaultSelectedKeys={['highlighted']}
  selectedColor="color-mix(in oklab, var(--Labels-Primary) 8%, transparent)"
  treeData={[
    { key: 'default', title: '默认项' },
    { key: 'highlighted', title: '使用自定义色的选中项' },
    { key: 'another', title: '其他项' },
  ]}
/>

自定义节点图标​

每个 TreeDataNode 可通过 icon 传入任意 ReactNode,渲染在单一 20×20 槽位。建议尺寸 size-5(20px),颜色 text-(--Labels-Tertiary)。在 Tree 上设置 showIcon={false} 可关闭整棵树的节点图标。

展开 chevron 与节点 icon 在 folder 行共享同一槽位:默认展示 icon,行 hover / focus-within 时 chevron 以 opacity 渐变淡入覆盖 icon。叶子节点和 disabled folder 始终展示 icon。

icon 还可传函数 ({ expanded, hover }) => ReactNode,典型用法是按 expanded 切换"文件夹关 / 开"图标:

结果
Loading...
实时编辑器
<Tree
  defaultExpandedKeys={['root']}
  treeData={[
    {
      key: 'root',
      title: '工作区',
      icon: ({ expanded }) => (
        <svg
          className="size-5 text-(--Labels-Tertiary)"
          viewBox="0 0 20 20"
          fill="none"
          aria-hidden
        >
          {expanded ? (
            <path
              d="M2.5 6.5H8L9.5 4.5H17.5V15.5C17.5 16.0523 17.0523 16.5 16.5 16.5H3.5C2.94772 16.5 2.5 16.0523 2.5 15.5V6.5Z"
              stroke="currentColor"
              strokeWidth="1.2"
              strokeLinejoin="round"
            />
          ) : (
            <path
              d="M2.5 5.5C2.5 4.94772 2.94772 4.5 3.5 4.5H8L9.5 6.5H16.5C17.0523 6.5 17.5 6.94772 17.5 7.5V15.5C17.5 16.0523 17.0523 16.5 16.5 16.5H3.5C2.94772 16.5 2.5 16.0523 2.5 15.5V5.5Z"
              stroke="currentColor"
              strokeWidth="1.2"
              strokeLinejoin="round"
            />
          )}
        </svg>
      ),
      children: [
        {
          key: 'root/docs',
          title: '说明文档',
          icon: <Plus className="size-5 text-(--Labels-Tertiary)" aria-hidden />,
        },
        {
          key: 'root/api',
          title: '接口说明',
          icon: (
            <svg
              className="size-5 text-(--Labels-Tertiary)"
              viewBox="0 0 20 20"
              fill="none"
              aria-hidden
            >
              <path
                d="M5 4.5H15V15.5H5V4.5Z"
                stroke="currentColor"
                strokeWidth="1.2"
                strokeLinejoin="round"
              />
              <path
                d="M7 7.5H13M7 10H11M7 12.5H12"
                stroke="currentColor"
                strokeWidth="1.2"
                strokeLinecap="round"
              />
            </svg>
          ),
        },
      ],
    },
  ]}
/>

鼠标悬浮上面任意 folder 行,可以看到 chevron 在 folder 图标上方淡入。

Hover Actions​

TreeDataNode.actions 用于行末尾的 hover-only 操作位(对应 Figma "Hover - More" 变体),仅在 hover / focus-within 或浮层打开时显示,通过内部 ml-auto 始终贴近行右端。actions 内部的点击不会冒泡触发节点的 onSelect / onExpand。disabled 行不会显示 actions。

当 action 会打开 DropdownMenu / Dialog 等浮层时,请使用函数形态 (state) => ReactNode,并把浮层的 onOpenChange 接到 state.setActionsOpen。这样浮层打开期间行会保持 active 视觉(背景、actions、chevron 与 hover 一致),避免焦点跳到 portal 之后行立即回到 idle 的反直觉表现。

行的 selection 与 actions 是相互独立的——点击 actions 内按钮不会触发选中。setActionsOpen 只控制浮层打开期间的视觉延续。

打开下拉菜单​

把 "more" 触发按钮包在 DropdownMenu 中,传入声明式 items。把 onOpenChange 接到 setActionsOpen,菜单打开期间行保持 active 视觉。

结果
Loading...
实时编辑器
function MoreActionsDropdownDemo() {
  const buildMoreAction =
    (label) =>
    ({ setActionsOpen }) => (
      <DropdownMenu
        onOpenChange={setActionsOpen}
        items={[
          { label: '重命名', onSelect: () => alert(`重命名 ${label}`) },
          { label: '复制', onSelect: () => alert(`复制 ${label}`) },
          { type: 'separator' },
          { label: '删除', variant: 'destructive', onSelect: () => alert(`删除 ${label}`) },
        ]}
      >
        <button
          type="button"
          className="size-5 inline-flex items-center justify-center rounded-(--Radius_5) text-(--Labels-Tertiary) hover:bg-(--Grays-Gray-2) outline-none"
          aria-label={`${label} 的更多操作`}
        >
          ⋯
        </button>
      </DropdownMenu>
    )

  return (
    <Tree
      defaultExpandedKeys={['workspace']}
      treeData={[
        {
          key: 'workspace',
          title: '工作区',
          actions: buildMoreAction('工作区'),
          children: [
            { key: 'workspace/inbox', title: '收件箱', actions: buildMoreAction('收件箱') },
            { key: 'workspace/drafts', title: '草稿', actions: buildMoreAction('草稿') },
          ],
        },
      ]}
    />
  )
}

点击打开 Modal​

把触发按钮包在 Dialog(即 Modal)中,按钮只负责打开 modal,行 onSelect / onExpand 不受影响。把 Dialog 的 onOpenChange 接到 setActionsOpen,modal 打开期间行保持 active 视觉。

结果
Loading...
实时编辑器
function MoreActionsModalDemo() {
  const [activeKey, setActiveKey] = React.useState(null)

  const buildSettingsTrigger =
    (node) =>
    ({ setActionsOpen }) => (
      <Dialog
        title={`目录设置 — ${node.title}`}
        onOpenChange={setActionsOpen}
        content={
          <div className="flex flex-col gap-(--Spacing_8) text-(--Labels-Secondary)">
            <p>已打开「{node.title}」的设置面板。</p>
            <p>这里挂载真实表单字段;关闭后会回到树视图,选中状态不丢。</p>
          </div>
        }
        okText="保存"
        cancelText="取消"
        onOk={() => setActiveKey(node.key)}
      >
        <button
          type="button"
          className="size-5 inline-flex items-center justify-center rounded-(--Radius_5) text-(--Labels-Tertiary) hover:bg-(--Grays-Gray-2) outline-none"
          aria-label={`${node.title} 设置`}
        >
          ⋯
        </button>
      </Dialog>
    )

  const nodes = [
    { key: 'projects', title: '项目' },
    { key: 'archive', title: '归档' },
  ]

  return (
    <div className="flex flex-col gap-(--Spacing_8)">
      <Tree treeData={nodes.map((node) => ({ ...node, actions: buildSettingsTrigger(node) }))} />
      {activeKey != null && (
        <div className="text-(length:--Font-Size-Caption) text-(--Labels-Tertiary)">
          最近保存:<code>{activeKey}</code>
        </div>
      )}
    </div>
  )
}

超长标题​

Tree 的宽度完全由外层容器决定,组件自身没有内置 max-width。当 title 文本溢出时,右侧边缘通过 CSS mask-image 渐变淡出,提示还有更多内容。hover 时 actions 按钮绝对定位叠加在淡出区域上方,不影响 title 的布局宽度。

超长的 title 还会自动加 tooltip(默认悬停延迟 500ms)展示完整内容。组件在指针进入或获得焦点时,通过「克隆离屏节点测自然宽度 vs 容器宽度」判断是否超长(而非 scrollWidth),因此与渐变遮罩及任意 ReactNode title 都兼容。未超长的 title 不会引入 tooltip。

通过每个 TreeDataNode 的 tooltip 字段配置对应节点自动 Tooltip 的定位、延迟、碰撞策略和样式;所有 placement 预设均固定使用 sideOffset: 8,并优先于底层 side / align / sideOffset 配置。标题内容、Trigger 结构及受控开关仍由 Tree 管理。左侧导航中的节点推荐优先向右展示;当首选方向空间不足时,仍允许 Radix 的默认碰撞策略自动翻转,避免内容越出视口。

结果
Loading...
实时编辑器
function LongTitleDemo() {
  const tooltip = { placement: 'right' }
  const buildAction =
    (label) =>
    ({ setActionsOpen }) => (
      <DropdownMenu
        onOpenChange={setActionsOpen}
        items={[
          { label: '重命名', onSelect: () => alert(`重命名 ${label}`) },
          { label: '删除', variant: 'destructive', onSelect: () => alert(`删除 ${label}`) },
        ]}
      >
        <button
          type="button"
          className="size-5 inline-flex items-center justify-center rounded-(--Radius_5) text-(--Labels-Tertiary) hover:bg-(--Grays-Gray-2) outline-none"
          aria-label={`${label} 的更多操作`}
        >
          ⋯
        </button>
      </DropdownMenu>
    )

  return (
    <div style={{ width: 220 }}>
      <Tree
        defaultExpandedKeys={['folder']}
        treeData={[
          {
            key: 'long1',
            title: '这条录音有一个非常非常长的标题会发生溢出',
            tooltip,
            actions: buildAction('long1'),
          },
          {
            key: 'folder',
            title: '项目文件夹名字也很长也会溢出',
            tooltip,
            actions: buildAction('folder'),
            children: [
              {
                key: 'child1',
                title: '子节点的标题同样很长也会发生溢出',
                tooltip,
                actions: buildAction('child1'),
              },
              { key: 'child2', title: '短标题' },
            ],
          },
        ]}
      />
    </div>
  )
}

鼠标悬浮任意项可看到 actions 按钮出现在渐变区域上方,悬停超长标题 500ms 可在首选右侧看到完整内容 Tooltip。调整容器 width 可测试不同宽度下的碰撞回退表现。

受控展开​

通过 expandedKeys + onExpand 完全受控展开;selectedKeys + onSelect 完全受控选中。

结果
Loading...
实时编辑器
function ControlledTree() {
  const [expandedKeys, setExpandedKeys] = React.useState(['root'])
  const [selectedKeys, setSelectedKeys] = React.useState([])

  return (
    <Tree
      treeData={[
        {
          key: 'root',
          title: '工作区',
          children: [
            { key: 'docs', title: '文档' },
            { key: 'audio', title: '音频' },
          ],
        },
      ]}
      expandedKeys={expandedKeys}
      selectedKeys={selectedKeys}
      onExpand={(keys) => setExpandedKeys(keys)}
      onSelect={(keys) => setSelectedKeys(keys)}
    />
  )
}

拖拽​

使用原生 HTML5 DnD,无需额外依赖。传入 draggable 与 onDrop;组件不会改写 treeData,请在回调里用 moveTreeNode(自 @plaud/design 导出)或自行拼装新数组。每行纵向按 25% / 50% / 25% 划分 before / inside / after;叶子行将中部合并为下半区。禁止将祖先拖入其后代。**禁用行不可作为 drop 投放目标。**折叠 folder 仅在中部 inside 区连续 dragOver 满 500ms 时触发 onExpand(受控模式下请自行更新 expandedKeys);上下间隙带不会自动展开。

拖拽视觉遵循 Figma 规范:拖拽源行显示 1px Separators/Emphasized 内描边;行间投放的 Labels/Link 圆点 + 横线指示器会对中到实测物理间隙(普通行为 4px,folder 行后可为 8px),inside 投放时整行高亮。可见指示器是最终落点的唯一事实源;即使松手点直接落在 Tree 根容器的行间空白,或 release 坐标发生像素漂移,也不会改变已显示的目标位置。浏览器默认拖拽影像会被替换为由行内 icon + title 构建的白底阴影预览浮层(经 setDragImage 注入)。

结果
Loading...
实时编辑器
function DragTree() {
  const [treeData, setTreeData] = React.useState([
    { key: 'a', title: 'Alpha' },
    {
      key: 'b',
      title: 'Beta',
      children: [
        { key: 'b1', title: 'Beta-1' },
        { key: 'b2', title: 'Beta-2' },
      ],
    },
  ])
  const [expandedKeys, setExpandedKeys] = React.useState(['b'])

  return (
    <Tree
      treeData={treeData}
      expandedKeys={expandedKeys}
      onExpand={(keys) => setExpandedKeys(keys)}
      draggable
      onDrop={(info) => {
        setTreeData((prev) =>
          moveTreeNode(prev, info.dragNode.key, info.dropNode.key, info.dropPosition),
        )
      }}
    />
  )
}

外部拖入​

传入 onExternalDrop 即可接受从树外部拖入的元素(例如右侧文件列表)。回调提供 dropNode、dropPosition 以及原始 DragEvent,调用方从 event.dataTransfer 中读取自定义拖拽数据。allowExternalDrop 可限制哪些 drop 区域有效。折叠 folder 悬停 500ms 同样会自动展开。

注意:Photos、Videos 等叶子节点(无 children)的 drop position 被强制为 -1 / 1,永远不会是 0。若需要限制"只投放到文件夹内部",allowExternalDrop 应同时兼容叶子节点,例如:

allowExternalDrop={({ dropNode, dropPosition }) =>
dropPosition === 0 || !dropNode.children?.length
}
结果
Loading...
实时编辑器
function ExternalDropDemo() {
  const [expandedKeys, setExpandedKeys] = React.useState([])
  const [log, setLog] = React.useState(null)

  const folders = [
    {
      key: 'documents',
      title: '文档',
      children: [
        { key: 'work', title: '工作' },
        { key: 'personal', title: '个人' },
      ],
    },
    {
      key: 'media',
      title: '媒体',
      children: [
        { key: 'photos', title: '图片' },
        { key: 'videos', title: '视频' },
      ],
    },
    {
      key: 'archive',
      title: '归档',
      children: [{ key: 'archive-2023', title: '2023' }],
    },
  ]

  const files = [
    { key: 'report.pdf', label: '📄 report.pdf' },
    { key: 'photo.jpg', label: '🖼 photo.jpg' },
    { key: 'notes.txt', label: '📝 notes.txt' },
    { key: 'video.mp4', label: '🎬 video.mp4' },
  ]

  return (
    <div style={{ display: 'flex', gap: 16, alignItems: 'flex-start' }}>
      {/* 左侧:文件夹树 */}
      <div style={{ width: 200, border: '1px solid #e5e5e5', borderRadius: 8, padding: '8px 0' }}>
        <div style={{ padding: '0 12px 6px', fontSize: 12, color: '#999' }}>文件夹</div>
        <Tree
          treeData={folders}
          expandedKeys={expandedKeys}
          onExpand={(keys) => setExpandedKeys(keys)}
          onExternalDrop={({ dropNode, dropPosition, event }) => {
            const fileKey = event.dataTransfer.getData('application/x-file-key')
            const posLabel = dropPosition === 0 ? '放入' : dropPosition === -1 ? '插入前' : '插入后'
            setLog(`"${fileKey}" → ${dropNode.title}(${posLabel})`)
          }}
        />
      </div>

      {/* 右侧:文件列表 */}
      <div style={{ flex: 1 }}>
        <div style={{ fontSize: 12, color: '#999', marginBottom: 8 }}>
          文件 — 拖入左侧文件夹(悬停折叠文件夹 1s 可展开)
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
          {files.map((file) => (
            <div
              key={file.key}
              draggable
              onDragStart={(e) => {
                e.dataTransfer.setData('application/x-file-key', file.key)
                e.dataTransfer.effectAllowed = 'copy'
              }}
              style={{
                padding: '6px 12px',
                border: '1px solid #e5e5e5',
                borderRadius: 6,
                cursor: 'grab',
                fontSize: 14,
                background: '#fafafa',
                userSelect: 'none',
              }}
            >
              {file.label}
            </div>
          ))}
        </div>
        {log && (
          <div
            style={{
              marginTop: 12,
              fontSize: 13,
              padding: '6px 10px',
              background: '#f0fdf4',
              border: '1px solid #bbf7d0',
              borderRadius: 6,
              color: '#166534',
            }}
          >
            ✓ {log}
          </div>
        )}
      </div>
    </div>
  )
}

大数据量树​

节点上千时,任何祖先布局失效都很贵 —— 即使子树远在屏幕外,浏览器仍要遍历它的结构性盒子。Tree 会自动跳过这部分工作,没有开关需要打开。

每棵子树都带 content-visibility: auto,浏览器因此省掉它的渲染工作(rendering work,含 style、layout、paint),而元素自身仍保留一个高度精确的占位盒。

与虚拟列表不同,节点始终留在 DOM 里。这不是浏览器的实现细节,而是 CSS Containment 规范的强制要求:auto 下被跳过的内容「必须照常可被 find-in-page、Tab 序等用户代理功能访问,且可聚焦、可选中」。find-in-page 命中时浏览器会自动解除跳过并滚动过去 —— 这正是它在「节点文字必须可被浏览器搜索」时优于虚拟化的原因。

跳过依赖两条浏览器支持面不同的 CSS 声明——content-visibility: auto(Chrome/Edge 85+、Firefox 125+、Safari 18+)与 contain-intrinsic-size(Safari 17+)。两条都在挂载时做特性检测;缺任何一条的浏览器就照常完整渲染,不产生任何 console 噪音。目前约 6% 的流量落在这里。

占位高度由 --pd-tree-row-height 解析而来,因此只在真实行高与该变量一致时才准。一旦不一致,Tree 会回退成完整渲染并在这棵树的余生保持该状态:慢,但是对的。什么会破坏这个一致、回退如何被发现,见下方「自定义行高」。

自定义行高​

行高可以改,覆盖 --pd-tree-row-height 即可:

<Tree className="[--pd-tree-row-height:72px]" treeData={treeData} />

占位尺寸是基于同一个变量的 calc(),所以行自身的 min-height 与屏外占位尺寸永远解析到同一个值,不可能失配。

不要用别的方式压行高。下面这些同样能改出视觉(后代选择器特异性还更高),但都绕过了变量,占位尺寸会停在旧值,导致 scrollHeight 塌缩、滚动位置丢失(并触发回退):

绕过写法为什么不行
[&_.pd-tree-item]:min-h-* / h-* / py-*压过组件自身的类,占位尺寸仍按变量算
调用方样式表改 .pd-tree-item(尤其 !important)同上,且静态检查看不到
node.className 里带高度类只影响单行,占位尺寸仍按变量算

真实行高与声明变量不一致时,Tree 会回退成完整渲染,并在开发环境告警:挂载后测第一行,之后每一行重新回到屏幕上时也测 —— 监听 contentvisibilityautostatechange,每 0.5s 的事件合成一批处理,所以列表深处那一行超高也抓得到,而不只是第一行。这个检查校验的是结果,所以样式表、!important、字体切换、以及内容本身超过行高等情况都能覆盖。

有一条性质组件只能检测、无法强制:同一棵树内行高必须一致。 占位尺寸按单一行高解析;逐行测量是 O(n) 强制布局,会成为新的卡顿源。所以行高不一的树会在那个不同高的行进入屏幕时触发回退,此后不再享受优化。

content-visibility: auto 同时带来 paint containment,而每个子树 wrapper 的盒与行盒完全重合。于是画在行盒之外的装饰会被四边整圈裁掉:外扩 ring、带 outline-offset 的 outline、外阴影,一个像素都不剩。它不报错、computed style 照旧报着颜色和宽度,只是画不出来。组件自身的键盘焦点环因此画在行盒之内。若你通过 node.className 传装饰,请改用内嵌变体(inset-ring-* / inset-shadow-*)。

完全不该占布局的节点 —— 比如只为让父节点渲染展开箭头而存在的隐形占位行 —— 必须标 hidden: true 而不是传一个 hidden class,这样树才能把它排除在上述高度计算之外。只藏行的话 wrapper 仍会吃掉一个 gap;跳过既然是默认行为,这条要求也就是无条件的。

要确认某个节点是否真的被跳过,请监听 contentvisibilityautostatechange 事件,不要去读几何反推 —— 读取布局会强制元素渲染,把你正要测量的状态本身抹掉。

尺寸 & Token​

元素取值
行高min-height 32px(--pd-tree-row-height,默认取 Spacing_32)
行内边距左 8px / 右 4px
行内 gap(槽位 / 标题)8px(Spacing_8)
层级缩进首层子级 12px,此后每层 +16px(indent 默认 16)
左侧槽位(chevron / icon 共用)20×20
字体Body 14 / 20(Font-Size-Body / Line-Height-Body)
Drop 指示器∅8 圆点(2px 描边)+ 2px 横线,对中到实测 4px / 8px 物理行间隙
元素Token
标题颜色(Default)Labels/Primary#000000
Switcher / Icon 颜色Labels/Tertiary#757575
Hover 背景Grays/Gray-1#EBEBEB
Selected 背景(默认)Grays/Gray-1#EBEBEB
拖拽源描边Separators/Emphasized#CCCCCC
Drop 指示器(圆点 + 横线)Labels/Link#1573D1
拖拽预览背景Grays/White#FFFFFF

Props​

Prop类型默认值说明
treeDataTreeDataNode[]-树数据(必填)
expandedKeysstring[]-受控展开 keys
defaultExpandedKeysstring[][]默认展开 keys
defaultExpandAllbooleanfalse是否默认展开所有可展开节点
selectedKeysstring[]-受控选中 keys
defaultSelectedKeysstring[][]默认选中 keys
onExpand(keys, { node, expanded }) => void-展开 / 折叠回调
onSelect(keys, { node, selected }) => void-选中回调
draggableboolean | ((node: TreeDataNode) => boolean)-启用拖拽源;函数形式可按节点过滤
allowDrop(info: TreeAllowDropInfo) => boolean-返回 false 拒绝 drop(成环由组件兜底)
onDragStart(info: TreeDragNodeInfo) => void-行上开始拖拽
onDragEnter(info: TreeDragEnterInfo) => void-拖拽进入某行
onDragLeave(info: TreeDragNodeInfo) => void-拖拽离开某行
onDragEnd(info: TreeDragNodeInfo) => void-拖拽结束(含 drop 之后)
onDrop(info: TreeDropInfo) => void-成功 drop;由调用方更新 treeData
allowExternalDrop(info: TreeExternalAllowDropInfo) => boolean-返回 false 拒绝外部 drop
onExternalDrop(info: TreeExternalDropInfo) => void-外部元素投放回调;从 info.event.dataTransfer 读取拖拽源数据
showIconbooleantrue是否渲染节点 icon
indentnumber16每层缩进像素;按 Figma 规范首层子级实际缩进 indent - 4
selectedColorstringvar(--Grays-Gray-1)选中行背景色,接受任意 CSS color / var() 表达式
classNamestring-根 className

TreeDropPosition 取值为 -1(插入到目标前)|0(作为目标首子)|1(插入到目标后)。TreeDropInfo 含 dropToGap 与 isCrossParent。不可变工具:moveTreeNode(treeData, dragKey, dropKey, dropPosition)、isDescendant(treeData, ancestorKey, candidateKey) 与 resolveDropPosition({ clientY, rowTop, rowHeight, isLeafRow })(与组件内 drop 几何一致)由 @plaud/design 导出。

TreeDataNode​

字段类型说明
keystring节点唯一标识(必填)
titleReactNode节点标题(必填)。计数 / 副标题等附属信息直接拼到 title 里渲染。
hiddenboolean该行完全不参与布局(display: none 打在节点 wrapper 上)。用于「仅为让父节点渲染展开箭头而注入的隐形占位子节点」。不要改用 className: 'hidden':只藏行的话 wrapper 仍是 flex item、照样占一个 gap,会让屏外占位高度算错。
tooltipTreeTitleTooltipProps当前节点自动 Tooltip 的安全配置。placement 支持 top / right / bottom / left 及每个方向的 -start / -end 变体,解析为 Tooltip 的 side / align 与 sideOffset: 8;内容、Trigger 结构及受控开关由 Tree 管理。
iconReactNode | ((state: { expanded; hover }) => ReactNode)节点前置图标(渲染在 20×20 槽位;folder 行 hover 时该槽位以 opacity 渐变切换为 chevron)。函数形态可按 expanded / hover 切换图标。
actionsReactNode | ((state: TreeNodeActionsState) => ReactNode)hover-only 操作位(如 more 菜单)。hover / focus-within 或行内浮层打开期间显示;内部点击不冒泡触发节点 select / expand。函数形态接收 setActionsOpen,把它接到浮层 onOpenChange 即可在浮层打开期间维持 active 视觉。
childrenTreeDataNode[]子节点
isLeafboolean强制声明叶子节点
disabledboolean禁用节点(不可选中 / 不可拖拽 / 不可作为 drop 投放目标 / 不响应指针;hover 也不切到 chevron 或 actions)
classNamestring节点 className