# useContainerReveal

## 使用方法

一个不带外观的「悬停或聚焦才露出」原语。给容器挂上一段作用域内的触发逻辑：鼠标停在容器上，或者键盘焦点进到容器里，容器里的内容就露出来（或者反过来藏起来），也就是常见的「行操作悬停才出现」。露出完全由 CSS 完成：Vue 里不存悬停状态，悬停也不会引起重新渲染。调用方不用为露出写任何 CSS，hook 会给容器的类，也会给内容的类；嵌套的容器盖住外层，所以两层互不串味。无障碍上是自带的：藏起来的内容用定位和不透明度藏（绝不 `display: none`），所以它仍然挂在树上、仍然在 Tab 顺序里、读屏也读得到；`:focus-within` 时会露出，键盘 Tab 进来就能看见；触摸设备上一直可见（不会因为粗指针没有 hover 而丢掉）；也尊重 `prefers-reduced-motion`。

`containerProps()` 返回容器的 `class` 和 `style`，`contentProps()` 返回每个待露出子元素的 `class`。分别用 `:class` 和 `:style` 绑上去。

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

## 建议

| 指引 | 实践 |
| --- | --- |
| 推荐 | 取出 `containerProps` 和 `contentProps`：`containerProps()` 绑在容器上（和你自己的类一起），`contentProps()` 绑在要露出的内容上。 |
| 推荐 | 只用来放次要操作：列表或表格行上的编辑、复制、删除，卡片或媒体块上的浮层控件（例如 `Thumbnail` 的删除按钮）。 |
| 推荐 | 组件自己的 prop 决定「悬停才露出」还是「一直显示」时，用 `isEnabled` 关掉它；它随时可以变。 |
| 推荐 | 绝对定位或浮层类的内容传 `isLayoutPreserved`，提前占好位置，出现时不会把布局顶动。 |
| 推荐 | 长列表的行设一个 `hoverDelay`（100-250 毫秒），鼠标扫过列表时就不会一路点亮每一行；键盘和触摸仍然立即露出。 |
| 推荐 | 交互由别的东西掌管时（拖拽、滚动或动效门控、行的菜单已打开）用 `forceState`；只要某一个元素不跟容器走时用 `forceVisibility`。 |
| 避免 | 绕过 API 去改 hook 内部的私有自定义属性（`--_reveal-opacity` 之类）来压住露出；用 `forceState` / `forceVisibility`，改名也不会坏。 |
| 避免 | 用它藏那些必须随时能被发现的内容；重要操作就一直显示，别挂在悬停后面。 |

## 示例

### useContainerReveal — 悬停露出的行操作

文件行平时把编辑和删除藏起来，悬停或用键盘聚焦时通过 useContainerReveal 露出；操作一直在树上，也在 Tab 顺序里。

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

const files = ref([
  { name: 'report.pdf', meta: 'Edited 2 hours ago' },
  { name: 'budget.xlsx', meta: 'Edited 2 hours ago' },
  { name: 'notes.txt', meta: 'Edited 2 hours ago' },
])

const reveal = useContainerReveal()
const container = reveal.containerProps({ hoverDelay: 150 })
const content = reveal.contentProps()

function remove(name: string): void {
  files.value = files.value.filter(file => file.name !== name)
}
</script>

<template>
  <ul class="reveal-preview">
    <li
      v-for="file in files"
      :key="file.name"
      class="reveal-preview__row"
      :class="container.class"
      :style="container.style"
    >
      <span class="reveal-preview__label">
        <span class="reveal-preview__name">{{ file.name }}</span>
        <span class="reveal-preview__meta">{{ file.meta }}</span>
      </span>
      <span class="reveal-preview__actions" :class="content.class">
        <IconButton label="Edit" size="sm" variant="ghost">
          <Icon icon="i-heroicons-pencil-square" />
        </IconButton>
        <IconButton label="Delete" size="sm" variant="ghost" @click="remove(file.name)">
          <Icon icon="i-heroicons-trash" />
        </IconButton>
      </span>
    </li>
  </ul>
</template>

<style scoped>
.reveal-preview {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  max-width: 420px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.reveal-preview__row {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 44px;
  padding: 6px 10px;
  border-radius: 8px;
}

.reveal-preview__row:hover {
  background: var(--astryx-color-background-muted);
}

.reveal-preview__label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.reveal-preview__name {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--astryx-color-text-primary);
}

.reveal-preview__meta {
  font-size: 0.75rem;
  color: var(--astryx-color-text-secondary);
}

.reveal-preview__actions {
  display: flex;
  align-items: center;
  gap: 2px;
  top: 0;
  bottom: 0;
  right: 10px;
}
</style>
```

## 参数

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `options` | `UseContainerRevealOptions` | `{}` | 露出的配置对象，可以不传。 |
| `options.isEnabled` | `MaybeRefOrGetter<boolean>` | `true` | 为 false 时这个 hook 完全失效：容器拿不到类，内容也拿不到类，内容一直可见。每次调用都会重新读，所以组件挂载后也能改（例如按 `revealOn === "hover"` 决定）。 |
| `containerProps(options?).hoverDelay` | `number` | — | 指针要停多少毫秒才开始露出，和 Tooltip、HoverCard 的延时一样，是个「确认要悬停」的闸门：光标扫过列表时不会把沿途的行都点亮。键盘和粗指针不等待。 |
| `containerProps(options?).forceState` | `'active' \| 'inactive'` | — | 把触发状态钉住，交给调用方掌管的时候用：动效门控、滚动中、行的菜单已打开。`'inactive'` 仍然让键盘焦点和粗指针看见内容。 |
| `contentProps(options?).isRevealInverted` | `boolean` | `false` | 反过来：平时可见，悬停时藏起。只影响指针，键盘焦点和触摸照样能看到。 |
| `contentProps(options?).isLayoutPreserved` | `boolean` | `false` | 藏起时仍占住原来的位置（只改不透明度），避免布局跳动。 |
| `contentProps(options?).forceVisibility` | `'shown' \| 'hidden'` | — | 单独钉住这一个元素的可见性，不跟容器走。`'hidden'` 在焦点进来时仍会露出。 |

## 返回值

| 名称 | 类型 | 说明 |
| --- | --- | --- |
| `containerProps` | `(options?: ContainerRevealOptions) => { class?: string \| Record<string, boolean>, style?: CSSProperties }` | 绑在容器上，容器的悬停和 focus-within 决定露出。可以传 `hoverDelay` 和 `forceState`。hook 关掉时返回空对象。 |
| `contentProps` | `(options?: ContentRevealOptions) => { class?: string \| Record<string, boolean> }` | 绑在每一个要露出或要藏起的子元素上。可以传 `isRevealInverted`、`isLayoutPreserved` 和 `forceVisibility`。hook 关掉时返回空对象。 |
