# 大纲

## 使用方法

`Outline` 是目录。每一项是 `{ id, label, level }`。`id` 对应页面上标题元素的 id。`level` 从 1 到 6。1 和 2 共用第一档缩进，3、4、以及 5–6 再逐档往里收。

不传 `activeId` 时，大纲跟着滚动走。传了 `scrollContainer` 就盯这个容器，否则找最近的可滚动祖先，再没有就用视口。当前项是顶部已经越过停留线的最后一个标题。滚到底时，最后一项为当前项。`offset` 把停留线和滚动落点一起下移，标题自己的 `scroll-margin-top` 仍然算在里面。

传入 `activeId` 后，滚动侦测关闭。`v-model:active-id` 把高亮留在你自己的状态里。点击开始滚动时发出 `navigateStart`。同一次导航结束时发出一次 `navigateEnd`：滚动停稳、读者中途打断，或超过 1.2 秒，都会结束。

`hasScrollOnClick` 默认打开。带修饰键的点击，或目标 id 在页面上不存在时，不接管浏览器自己的行为。激活一项会用 `history.pushState` 写入 hash。

`parseOutlineFromMarkdown` 从 Markdown 抽出目录，id 和 `Markdown` 写到标题上的锚点是同一套。围栏代码里的井号不算标题。引用或列表里的标题会占掉重复 id，但不出现在目录里。`useOutlineFromMarkdown` 在正文变化时重算。`useOutlineFromDOM` 读取容器里带 id 的 h1 到 h6。

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

const items = [
  { id: 'overview', label: 'Overview', level: 2 },
  { id: 'theming', label: 'Theming', level: 2 },
  { id: 'tokens', label: 'Tokens', level: 3 },
]
</script>

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

## 建议

| 建议 | 做法 |
| --- | --- |
| 推荐 | 每个 `id` 指向真实标题，`level` 和那个标题的级别一致。 |
| 推荐 | 页面有粘性顶栏时，把 `offset` 设成顶栏高度，当前项和滚动落点才对得上。 |
| 推荐 | 窄侧栏用 `density="compact"`，长文旁边用 `default`。 |
| 避免 | 传了 `activeId` 还指望内置滚动侦测继续改它。只要传入，侦测就关掉。 |
| 避免 | 再用额外标记自己缩进。级别已经决定缩进。 |

### 结构

- **大纲**: 装着本页标题链接和指示器的导航容器。
- **标题链接**: 指向一个标题的锚点，按层级缩进，当前项会标出来。
- **标签**: 标题链接里显示的标题文字。
- **指示轨道**: 当前指示器后面画出的竖线。
- **当前指示**: 滑到当前标题链接旁边的条。

## 示例

### 受控

传入 activeId 后，内置的滚动侦测会停下来，高亮由你自己的逻辑决定。

```vue
<script setup lang="ts">
import type { OutlineItem } from '@astryx-vue/core/Outline'
import { Button } from '@astryx-vue/core/Button'
import { Outline } from '@astryx-vue/core/Outline'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { computed, ref } from 'vue'

const items: OutlineItem[] = [
  { id: 'ctrl-summary', label: 'Summary', level: 2 },
  { id: 'ctrl-details', label: 'Details', level: 2 },
  { id: 'ctrl-results', label: 'Results', level: 2 },
  { id: 'ctrl-next-steps', label: 'Next steps', level: 2 },
]

const activeId = ref('ctrl-details')
const index = computed(() => items.findIndex(item => item.id === activeId.value))

function goTo(next: number): void {
  const clamped = Math.max(0, Math.min(items.length - 1, next))
  activeId.value = items[clamped].id
}
</script>

<template>
  <VStack :gap="4" :width="240">
    <Outline v-model:active-id="activeId" :items="items" />
    <HStack :gap="2">
      <Button variant="secondary" size="sm" label="Previous" @click="goTo(index - 1)" />
      <Button variant="secondary" size="sm" label="Next" @click="goTo(index + 1)" />
    </HStack>
    <Text type="supporting" color="secondary">
      Active section: {{ items[index]?.label }}
    </Text>
  </VStack>
</template>
```

### 深层嵌套

1 到 4 级标题逐级向内缩进，长文档里的小节仍然容易扫读。

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

const items: OutlineItem[] = [
  { id: 'deep-introduction', label: 'Introduction', level: 1 },
  { id: 'deep-concepts', label: 'Core concepts', level: 2 },
  { id: 'deep-tokens', label: 'Tokens', level: 3 },
  { id: 'deep-color', label: 'Color', level: 4 },
  { id: 'deep-spacing', label: 'Spacing', level: 4 },
  { id: 'deep-components', label: 'Components', level: 2 },
  { id: 'deep-primitives', label: 'Primitives', level: 3 },
  { id: 'deep-patterns', label: 'Patterns', level: 3 },
  { id: 'deep-resources', label: 'Resources', level: 1 },
]
</script>

<template>
  <div style="width: 240px">
    <Outline :items="items" active-id="deep-color" />
  </div>
</template>
```

### 密度

compact 收紧每一项的内边距，适合密集侧栏。default 是文档里的常规间距。指示条会跟着每一项的高度走。

```vue
<script setup lang="ts">
import type { OutlineItem } from '@astryx-vue/core/Outline'
import { Outline } from '@astryx-vue/core/Outline'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'

const items: OutlineItem[] = [
  { id: 'density-getting-started', label: 'Getting started', level: 2 },
  { id: 'density-configuration', label: 'Configuration', level: 2 },
  { id: 'density-api', label: 'API reference', level: 3 },
  { id: 'density-examples', label: 'Examples', level: 3 },
  { id: 'density-faq', label: 'FAQ', level: 2 },
]
</script>

<template>
  <HStack :gap="10" align="start">
    <VStack :gap="3" :width="200">
      <Text type="supporting" color="secondary" weight="medium">
        Default
      </Text>
      <Outline :items="items" density="default" active-id="density-configuration" />
    </VStack>
    <VStack :gap="3" :width="200">
      <Text type="supporting" color="secondary" weight="medium">
        Compact
      </Text>
      <Outline :items="items" density="compact" active-id="density-configuration" />
    </VStack>
  </HStack>
</template>
```

## 属性

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `items` | `OutlineItem[]` | — | 按顺序排列的标题。每一项是 `{ id, label, level }`。`level` 为 1–6。 |
| `activeId` | `string` | — | 受控的当前标题。一旦传入，内置滚动侦测关闭。用 `v-model:active-id` 绑定。 |
| `label` | `string` | `Table of contents` | 导航地标的无障碍名称。默认文案随语言变化，中文为「目录」。 |
| `density` | `'default' \| 'compact'` | `'default'` | 条目内边距。`compact` 用于密集侧栏。 |
| `offset` | `number` | `0` | 盖住滚动容器顶部的固定头高度，单位像素。停留线和滚动落点一起下移。 |
| `scrollContainer` | `HTMLElement \| null` | — | 要跟踪的滚动容器。不传则用最近的可滚动祖先，没有就用视口。 |
| `hasScrollOnClick` | `boolean` | `true` | 激活一项时是否平滑滚到对应标题。 |

## 事件

| 事件 | 参数 | 说明 |
| --- | --- | --- |
| `update:activeId` | `string` | 当前 id 变了。来源是滚动侦测，或读者激活了某一项。 |
| `navigateStart` | `string` | 一次点击开始滚向这个 id。 |
| `navigateEnd` | `string` | 这次导航结束、被打断，或到了 1.2 秒。每次开始只发一次。 |

## 主题

根类名是 `astryx-outline`。指示条读取 `--astryx-outline-indicator-top` 和 `--astryx-outline-indicator-height`，组件会按当前链接量出来。

缩进是 2px 的轨道，加上行内起始内边距：第一档为间距 3，然后是间距 7、间距 11、间距 12。`compact` 把块方向内边距从间距 2 改成间距 1。
