# 树形列表

## 使用方法

`TreeList` 用数据画出一棵树。每一项有 `id` 和 `label`。`children` 是下一层。`isExpanded` 是初始展开状态。点分支会切换展开。带 `href` 的叶子是链接。带 `onClick` 的叶子是按钮。

`variant="lineGuides"` 画连接线。`noGuides` 只保留缩进。`density` 改变行的上下内边距：紧凑 4px，适中 8px，宽松 12px。

`icon` 和 `endIcon` 可以是 `Icon` 的字形名，也可以是以 `i-` 开头的 CSS 图标类。`badge` 是末尾的短计数。内容比图标复杂时，用 `start` 和 `end` 插槽替换这两处字符串。

键盘遵循树的常见习惯：上下移动，左右展开和折叠（从右到左的布局里左右对调），Home 和 End 跳到两端，Enter 和空格激活，继续打字会跳到匹配的标签。

```vue
<script setup lang="ts">
import { TreeList } from '@astryx-vue/core/TreeList'

const items = [
  {
    id: 'src',
    label: 'src',
    isExpanded: true,
    children: [{ id: 'app', label: 'App.tsx', onClick: () => {} }],
  },
]
</script>

<template>
  <TreeList :items="items" />
</template>
```

## 建议

| 建议 | 做法 |
| --- | --- |
| 推荐 | 每一项给一个稳定的 `id`。焦点和展开都靠它跟踪。 |
| 推荐 | 层级深、只靠缩进不好认时用 `lineGuides`。 |
| 推荐 | 数量放进 `badge`，末尾的提示放进 `endIcon`，不要拼进标签。 |
| 避免 | 一行里又是链接又是按钮。一行只做一件事。 |
| 避免 | 很大的树默认全部展开。从读者当前所在的路径展开。 |

### 结构

- **树列表**: 呈现层级树的容器。
- **页眉**: 使用方提供的内容，用来给这棵树一个看得见的名字。
- **项**: 层级里一个节点的着色行。
- **箭头**: 有子节点的项上的展开和收起控件。
- **箭头符号**: 箭头里由 Icon 画出的方向符号。
- **项标签**: 标识一项的主要内容。
- **项说明**: 项标签下面的次要文字。
- **起始内容**: 画在项标签前面的、由使用方提供的内容。
- **尾部内容**: 画在项标签后面的、由使用方提供的内容。
- **引导线**: 表示父子关系的连接线。

## 示例

### TreeList — 带图标的文件树

文件树用文件夹和文档图标区分目录和文件。

```vue
<script setup lang="ts">
import type { TreeListItemData } from '@astryx-vue/core/TreeList'
import { TreeList } from '@astryx-vue/core/TreeList'

function noop() {}

const items: TreeListItemData[] = [
  {
    id: 'src',
    label: 'src',
    isExpanded: true,
    icon: 'i-heroicons-folder',
    children: [
      { id: 'app', label: 'App.tsx', onClick: noop, icon: 'i-heroicons-document' },
      { id: 'index', label: 'index.tsx', onClick: noop, icon: 'i-heroicons-document' },
    ],
  },
  { id: 'pkg', label: 'package.json', onClick: noop, icon: 'i-heroicons-document' },
]
</script>

<template>
  <TreeList :items="items" />
</template>
```

### TreeList — 可交互的设置

设置项可以点，文档那一行是链接。

```vue
<script setup lang="ts">
import type { TreeListItemData } from '@astryx-vue/core/TreeList'
import { TreeList } from '@astryx-vue/core/TreeList'

function noop() {}

const items: TreeListItemData[] = [
  {
    id: 'settings',
    label: 'Settings',
    isExpanded: true,
    icon: 'i-heroicons-cog-6-tooth',
    children: [
      { id: 'general', label: 'General', onClick: noop },
      { id: 'advanced', label: 'Advanced', onClick: noop },
    ],
  },
  {
    id: 'docs',
    label: 'Documentation',
    href: '#',
    endIcon: 'i-heroicons-chevron-right',
  },
]
</script>

<template>
  <TreeList :items="items" />
</template>
```

### TreeList — 邮箱

邮件文件夹，未读数量用徽标标出。

```vue
<script setup lang="ts">
import type { TreeListItemData } from '@astryx-vue/core/TreeList'
import { TreeList } from '@astryx-vue/core/TreeList'

function noop() {}

const items: TreeListItemData[] = [
  {
    id: 'inbox',
    label: 'Inbox',
    isExpanded: true,
    badge: '3',
    children: [
      { id: 'unread', label: 'Unread', onClick: noop, badge: '3' },
      { id: 'starred', label: 'Starred', onClick: noop },
    ],
  },
  { id: 'sent', label: 'Sent', onClick: noop },
  { id: 'drafts', label: 'Drafts', onClick: noop, badge: '1' },
]
</script>

<template>
  <TreeList :items="items" />
</template>
```

### TreeList — 导航

导航树里，当前页那一项是选中状态。

```vue
<script setup lang="ts">
import type { TreeListItemData } from '@astryx-vue/core/TreeList'
import { TreeList } from '@astryx-vue/core/TreeList'

function noop() {}

const items: TreeListItemData[] = [
  {
    id: 'nav',
    label: 'Navigation',
    isExpanded: true,
    children: [
      { id: 'home', label: 'Home', onClick: noop },
      { id: 'about', label: 'About', onClick: noop, isSelected: true },
      { id: 'contact', label: 'Contact', onClick: noop },
    ],
  },
]
</script>

<template>
  <TreeList :items="items" />
</template>
```

### Tree List — 变体

lineGuides 在父子之间画连接线。noGuides 只留缩进。密度是另一件事，只改行的内边距。

```vue
<script setup lang="ts">
import type { TreeListItemData } from '@astryx-vue/core/TreeList'
import { Stack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { TreeList } from '@astryx-vue/core/TreeList'

function noop() {}

const items: TreeListItemData[] = [
  {
    id: 'src',
    label: 'src',
    isExpanded: true,
    children: [
      {
        id: 'components',
        label: 'components',
        isExpanded: true,
        children: [
          { id: 'button', label: 'Button.tsx', onClick: noop },
          { id: 'card', label: 'Card.tsx', onClick: noop },
        ],
      },
      { id: 'app', label: 'App.tsx', onClick: noop },
    ],
  },
  { id: 'readme', label: 'README.md', onClick: noop },
]
</script>

<template>
  <Stack align="start" direction="horizontal" :gap="6" h-align="start" wrap="wrap">
    <Stack direction="vertical" :gap="2">
      <Text type="supporting" color="secondary" weight="bold">
        lineGuides (default)
      </Text>
      <TreeList :items="items" variant="lineGuides" />
    </Stack>
    <Stack direction="vertical" :gap="2">
      <Text type="supporting" color="secondary" weight="bold">
        noGuides
      </Text>
      <TreeList :items="items" variant="noGuides" />
    </Stack>
  </Stack>
</template>
```

## 属性

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `items` | `TreeListItemData[]` | - | 树的数据。字段见下表。 |
| `density` | `'compact' \| 'balanced' \| 'spacious'` | `'balanced'` | 一行的上下内边距：4px、8px 或 12px。 |
| `variant` | `'lineGuides' \| 'noGuides'` | `'lineGuides'` | 父子行之间是否画引导线。 |
| `header` | `string` | - | 树上方的纯文本标题。复杂内容用 `header` 插槽。 |
| `label` | `string` | - | 没有可见标题时，树的无障碍名称。 |

### TreeListItemData

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| <code>id</code> | <code>string</code> | 焦点和展开用的稳定标识。 |
| <code>label</code> | <code>string</code> | 可见名称。 |
| <code>description</code> | <code>string</code> | 第二行说明。 |
| <code>icon</code> | <code>string</code> | 前置字形名，或以 <code>i-</code> 开头的 CSS 图标类。 |
| <code>endIcon</code> | <code>string</code> | 末尾字形或 CSS 图标类。 |
| <code>badge</code> | <code>string</code> | 末尾计数。 |
| <code>children</code> | <code>TreeListItemData[]</code> | 子项。有这个字段的行可以展开。 |
| <code>href</code> / <code>target</code> | <code>string</code> | 把这一行渲染成链接。<code>target="_blank"</code> 会带上 <code>rel="noopener noreferrer"</code>。 |
| <code>onClick</code> | <code>(event: MouseEvent) =&gt; void</code> | 把叶子渲染成按钮。 |
| <code>isDisabled</code> | <code>boolean</code> | 这一行不接受焦点，也不能激活。 |
| <code>isSelected</code> | <code>boolean</code> | 由调用方标出当前项。树没有根上的选中值，也不强制只有一行选中。 |
| <code>isExpanded</code> | <code>boolean</code> | 初始是否展开。之后的切换由树自己记住。 |
| <code>className</code> | <code>string</code> | 额外类名，落在这一行上。 |
| <code>style</code> | <code>CSSProperties</code> | 行内样式，落在这一行上。 |

<code>start</code> 和 <code>end</code> 插槽收到 <code>{ item }</code>，用来替换 <code>icon</code> 和 <code>endIcon</code>。

读屏时一行的名字只有标题。说明通过 <code>aria-describedby</code> 单独读出，展开按钮和图标不进名字。敲字符跳转也只匹配标题。有可见 <code>header</code> 时，树的名字用这个标题，不用外面传来的 <code>aria-label</code>。在根上监听按键并 <code>preventDefault</code> 之后，方向键不再移动树里的焦点。

## 主题

缩进是 <code>--astryx-spacing-4</code>。行内容的左右内边距和间距都是 <code>--astryx-spacing-2</code>。引导线是 1px 的强调边框。焦点使用共用的焦点轮廓 token。选中行使用强调色的淡底。
