# 条目

## 使用方法

整个组件库里通用的「一行东西」：前面放内容、中间放名字和说明、后面再放内容。下拉菜单、选择器、联系人列表、通知、文件浏览、动态流，凡是要一行一行排的，都从它开始。

```ts
import { Item } from '@astryx-vue/core/List'
```

别的看起来像一行的东西都是拿它拼的。[列表项](/components/list-item) 在外面加了列表元素和行首标记；菜单、选择器、下拉结果各自补上父级要求的角色。这一行自己除了文字和插槽什么都不画，长什么样由外面那层面板决定。

### 结构

- **行首标记**: 可选的列表圆点或序号，排在前面那块内容之前。
- **前缀内容**: 前面那块：头像、图标、图片或者复选框。
- **主文字**: 这一行到底说的是什么。
- **说明**: 主文字下面那行补充文字。
- **末尾内容**: 靠右对齐的那块：徽章、时间或者操作按钮。

## 建议

| 指引 | 实践 |
| --- | --- |
| 推荐 | 常见排版用带名字的插槽（`startContent`、`label`、`description`、`endContent`），八成的场景这样写就够了。 |
| 推荐 | 菜单和密集列表用 `density="compact"`，普通行用 `"balanced"`，宽松布局用 `"spacious"`。 |
| 推荐 | 内容长短不固定时，用 `labelLines` 和 `descriptionLines` 控制在第几行截断。 |
| 推荐 | 前面或后面的内容比一行字更高时，用 `align="start"` 让它顶部对齐。 |
| 推荐 | 横滑出来的操作要在行里再留一个入口（菜单，或者 `endContent` 里的按钮）。横滑只是给手指的快捷方式，只能滑出来的操作，键盘和鼠标用户永远够不着。 |
| 避免 | 不要在可点的 `Item` 里再套按钮或链接。焦点和点击目标会变得难猜。 |
| 避免 | 不要拿 `Item` 做页面之间的跳转。那种场景用导航组件。 |
| 避免 | 不要把已读未读、收件箱之类的业务逻辑直接塞进来。包一层薄薄的组件（类似 PreviewItem）更好。 |

## 示例

常见的用法和不同的状态。

### 基础

一行名字、一行说明，末尾放个时间。适合简单、只需要对齐和间距一致的场景。

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

<template>
  <div class="item-basic">
    <Item label="Quarterly planning" description="Agenda, notes, and action items">
      <template #endContent>
        <Text color="secondary">
          Today
        </Text>
      </template>
    </Item>
    <Item label="Customer research" description="Interview notes from the latest study">
      <template #endContent>
        <Text color="secondary">
          Yesterday
        </Text>
      </template>
    </Item>
    <Item label="Launch checklist" description="Remaining tasks before release">
      <template #endContent>
        <Text color="secondary">
          Fri
        </Text>
      </template>
    </Item>
  </div>
</template>

<style scoped>
.item-basic {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
</style>
```

### 文档标签

单行、可以横向滚动的文档切换条：紧凑的选中行、前面的文件图标、悬停立刻出现且位置预留好的关闭按钮、稳定的分隔线，以及放在滚动区外面的新建按钮。适合编辑器标签、工作区标签、打开的文档、文件、画布和面板。

```vue
<script setup lang="ts">
import { Divider } from '@astryx-vue/core/Divider'
import { Icon } from '@astryx-vue/core/Icon'
import { IconButton } from '@astryx-vue/core/IconButton'
import { Item } from '@astryx-vue/core/List'
import { HStack } from '@astryx-vue/core/Stack'

const tabs = [
  { name: 'Project brief', isActive: true },
  { name: 'Research notes', isActive: false },
  { name: 'Interaction prototype', isActive: false },
]

function onSelect(): void {}
function onClose(): void {}
function onAdd(): void {}
</script>

<template>
  <HStack align="center" class="item-tabs" :gap="0.5">
    <HStack
      align="center"
      class="item-tabs__strip"
      role="group"
      aria-label="Open documents"
      :gap="0.5"
      :padding-inline="2"
      :padding-block="1"
      is-scrollable
    >
      <template v-for="(tab, index) in tabs" :key="tab.name">
        <Divider v-if="index > 0" orientation="vertical" class="item-tabs__rule" />
        <Item
          class="item-tabs__tab"
          :label="tab.name"
          density="compact"
          :is-selected="tab.isActive"
          :aria-current="tab.isActive ? 'true' : undefined"
          @click="onSelect"
        >
          <template #startContent>
            <Icon
              icon="i-carbon-document"
              size="sm"
              :color="tab.isActive ? 'primary' : 'secondary'"
            />
          </template>
          <template #endContent>
            <IconButton
              class="item-tabs__close"
              :label="`Close ${tab.name}`"
              variant="ghost"
              size="sm"
              @click.stop="onClose"
            >
              <Icon icon="close" size="sm" />
            </IconButton>
          </template>
        </Item>
      </template>
    </HStack>
    <IconButton label="New document" variant="ghost" size="sm" @click="onAdd">
      <Icon icon="i-carbon-add" size="sm" color="secondary" />
    </IconButton>
  </HStack>
</template>

<style scoped>
.item-tabs {
  min-width: 0;
  width: min(100%, 600px);
}

.item-tabs__strip {
  min-width: 0;
  scrollbar-width: none;
}

.item-tabs__rule {
  height: 16px;
}

.item-tabs__tab {
  flex: 0 1 auto;
  width: 164px;
  min-width: 96px;
}

/* The close button stays in the layout and only shows on hover or focus, so the
   label never shifts when the pointer enters the tab. */
.item-tabs__close {
  visibility: hidden;
}

.item-tabs__tab:hover .item-tabs__close,
.item-tabs__tab:focus-within .item-tabs__close,
.item-tabs__tab[data-selected] .item-tabs__close {
  visibility: visible;
}
</style>
```

### 带旁注

末尾对齐放状态和徽章。数量、状态、时间这些次要信息都放 endContent 插槽。

```vue
<script setup lang="ts">
import { Badge } from '@astryx-vue/core/Badge'
import { Icon } from '@astryx-vue/core/Icon'
import { Item } from '@astryx-vue/core/List'
import { Text } from '@astryx-vue/core/Text'
</script>

<template>
  <div class="item-metadata">
    <Item label="Build passed" description="Production deploy completed">
      <template #startContent>
        <Icon icon="check" size="sm" color="success" />
      </template>
      <template #endContent>
        <Text color="secondary">
          2m ago
        </Text>
      </template>
    </Item>
    <Item label="High memory usage" description="Worker pool is above the warning threshold" is-highlighted>
      <template #startContent>
        <Icon icon="warning" size="sm" color="warning" />
      </template>
      <template #endContent>
        <Badge label="Warning" variant="warning" />
      </template>
    </Item>
    <Item label="Sync failed" description="Retry after checking service credentials" is-selected>
      <template #startContent>
        <Icon icon="error" size="sm" color="error" />
      </template>
      <template #endContent>
        <Badge label="Action" variant="error" />
      </template>
    </Item>
  </div>
</template>

<style scoped>
.item-metadata {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
</style>
```

### 带前缀

前面带头像和图标。前面的东西要小，这样这一行才紧凑、好扫。

```vue
<script setup lang="ts">
import { Avatar } from '@astryx-vue/core/Avatar'
import { Badge } from '@astryx-vue/core/Badge'
import { Icon } from '@astryx-vue/core/Icon'
import { Item } from '@astryx-vue/core/List'
import { Text } from '@astryx-vue/core/Text'

function onSelect(): void {}
</script>

<template>
  <div class="item-start-content">
    <Item label="Ada Lovelace" description="Design systems engineer" @click="onSelect">
      <template #startContent>
        <Avatar name="Ada Lovelace" size="sm" />
      </template>
      <template #endContent>
        <Badge label="Owner" variant="purple" />
      </template>
    </Item>
    <Item label="Grace Hopper" description="Compiler platform" @click="onSelect">
      <template #startContent>
        <Avatar name="Grace Hopper" size="sm" />
      </template>
      <template #endContent>
        <Text color="secondary">
          Online
        </Text>
      </template>
    </Item>
    <Item label="Review handoff notes" description="Updated guidance is ready for the team" @click="onSelect">
      <template #startContent>
        <Icon icon="info" size="sm" color="secondary" />
      </template>
      <template #endContent>
        <Badge label="New" variant="blue" />
      </template>
    </Item>
  </div>
</template>

<style scoped>
.item-start-content {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
</style>
```

## 属性

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `label` | `string \| number` | — | 这一行的主文字。写字符串或数字时只显示一行；要放别的内容就用 `label` 插槽。 |
| `description` | `string \| number` | — | 主文字下面那行小字，同样只显示一行。要多行或者放别的内容就用 `description` 插槽。 |
| `as` | `ItemElement`（标签名或组件） | `'div'` | 根元素渲染成什么。只有在这一行自带 `role`、键盘交给父级管、并且行里不会多出一个 Tab 停留点时，才传组件；另外 `startContent` 和 `endContent` 里也不能放控件。 |
| `align` | `'center' \| 'start'` | `'center'` | 前面和后面那两块内容竖直方向怎么对齐。 |
| `density` | `'compact' \| 'balanced' \| 'spacious'` | `'balanced'` | 疏密。`compact` 上下各 4px，`balanced` 8px，`spacious` 上下左右各 12px。 |
| `layout` | `'stacked' \| 'inline'` | `'stacked'` | `stacked` 让说明单独占一行。`inline` 把说明和主文字放同一行，空间不够先截断说明，这样这一行能塞进高度固定的容器。 |
| `labelLines` | `number` | — | 主文字最多显示几行，超过就用省略号。不传时，字符串主文字只显示一行。 |
| `descriptionLines` | `number` | — | 说明最多显示几行，超过就用省略号。不传时，字符串说明只显示一行。 |
| `onClick` | `(event: MouseEvent) => void` | — | 点了之后做什么。给了它就渲染一个铺满行内容区的按钮，这一行也就有了按钮语义和悬停、按下样式。模板里写 `@click`。 |
| `interactiveRef` | `MaybeRefOrGetter` | — | 指向行里那个本来就能操作的控件，比如 `startContent` 里的复选框。这时整行变成更大的点击区域，点行面就转给那个控件，行自己不再渲染按钮。它和 `onClick`、`href` 只能用一个，写了它那两个会被忽略。 |
| `href` | `string` | — | 链接地址。行里会渲染一个链接元素。如果根元素本身就是链接组件（见 `as`），地址直接挂在根上，不再多画一个元素。 |
| `target` | `'_blank' \| '_self'` | — | 链接在哪打开，只有写了 `href` 才有用。`_blank` 会自动补上 `noopener` 和 `noreferrer`。 |
| `rel` | `string` | — | 链接的 rel 值，会和自动补的那些合在一起。 |
| `isDisabled` | `boolean` | `false` | 禁用。这一行不动了，内容变淡。 |
| `isSelected` | `boolean` | `false` | 选中状态。行的角色支持时写成 `aria-selected="true"`，不支持时写成 `aria-current="true"`。 |
| `isHighlighted` | `boolean` | `false` | 画出高亮状态，就是键盘聚焦和鼠标悬停时那个样子。 |
| `swipeActions` | `ItemSwipeActions` | — | 手指横滑能露出哪些操作，左右各一组，越靠后越靠外，每项是 `{ label, icon?, onActivate, isDisabled?, variant?, hasRemoval? }`。鼠标不会触发这次拖动。只有角色允许行内有可交互元素的才行，选项、菜单项、原生单选按钮的放大目标行都不能用。 |
| `swipeBehavior` | `'reveal' \| 'commit'` | `'reveal'` | `reveal` 松手后行停在滑开的位置，露出来的每项都是真按钮，拖得够远就触发最外面那个；`commit` 直接把行滑出去，松手就触发，什么都不留在屏幕上。 |

## 插槽

- `marker`：排在 `startContent` 前面，比如列表的圆点或者序号。
- `startContent`：前面那块，比如头像、图标、图片、复选框。
- `label`：自己画主文字，替掉 `label` 属性。
- `description`：自己画说明，替掉 `description` 属性。
- `endContent`：末尾对齐的那块，比如徽章、时间、操作按钮。

## 暴露

`element`（根元素）、`focus()` 和 `blur()`。聚焦会落到行里那个真正能接焦点的控件上。
