# useResizable

## 使用方法

给布局区域加上「拖着改大小」的能力。支持单区域和多区域；区域可以吸到固定的吸附点、可以整体收起，尺寸能存进 localStorage，多区域挤在一起时还能按顺序让出空间。

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

## 建议

| 指引 | 做法 |
| --- | --- |
| 推荐 | 想要 40% 宽、最窄 333px，写 `percent(40, { min: pixel(333) })`；想要 10% 宽、最宽 400px，写 `percent(10, { max: pixel(400) })`。第二个参数必填，且只能给下限或上限其中一个。 |
| 推荐 | 结构化的默认值只决定初始值；结构化的 `minSize` / `maxSize` 是持续生效的。状态、持久化、回调、`resize()`、绘制和 ARIA 用的都是解析后的像素数。 |
| 推荐 | `percent` 从 `@astryx-vue/core` 引入，Table 的那个 `pixel` 从 `@astryx-vue/core/Table` 引入。两处是同一个 pixel 绑定，各从自己的入口导出一次。 |
| 推荐 | 和 `Layout` 或 `AppShell` 的侧栏一起用，做可拖宽的导航面板。 |
| 推荐 | 想跨刷新记住用户拖出来的尺寸，设 `autoSaveId`。 |
| 避免 | 把 `minSize` 设得太小，内容会看不清。能整块藏起来的面板，改用 `collapsible`。 |

## 示例

常见配置、变体和状态。

### Resizable — 水平分割

水平方向的两栏分割，中间是一条可拖动的分隔条。

```vue
<script setup lang="ts">
import { useResizable } from '@astryx-vue/core'
import { Card } from '@astryx-vue/core/Card'
import { Heading } from '@astryx-vue/core/Heading'
import { Layout, LayoutContent, LayoutPanel } from '@astryx-vue/core/Layout'
import { ResizeHandle } from '@astryx-vue/core/Resizable'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'

const sidebar = useResizable({
  defaultSize: 200,
  minSize: 120,
  maxSize: 400,
})
</script>

<template>
  <Card :height="280" variant="muted" :width="600">
    <Layout height="fill">
      <template #start>
        <LayoutPanel :has-divider="false" :width="sidebar.size">
          <VStack :gap="2">
            <Heading :level="4">
              Sidebar
            </Heading>
            <Text color="secondary">
              {{ Math.round(sidebar.size) }}px wide
            </Text>
          </VStack>
        </LayoutPanel>
        <ResizeHandle direction="horizontal" has-divider label="Resize sidebar" :resizable="sidebar.props" />
      </template>
      <LayoutContent>
        <VStack :gap="2">
          <Heading :level="4">
            Content
          </Heading>
          <Text color="secondary">
            Drag the handle to resize the sidebar.
          </Text>
        </VStack>
      </LayoutContent>
    </Layout>
  </Card>
</template>
```

### Resizable — 可收起并带吸附点

用 useResizable 驱动的可收起侧栏，带吸附点。拖动会吸到预设宽度，拖过最小值就收起，expand 方法能把它恢复回来。

```vue
<script setup lang="ts">
import { useResizable } from '@astryx-vue/core'
import { Button } from '@astryx-vue/core/Button'
import { Card } from '@astryx-vue/core/Card'
import { Layout, LayoutContent, LayoutPanel } from '@astryx-vue/core/Layout'
import { ResizeHandle } from '@astryx-vue/core/Resizable'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'

const sidebar = useResizable({
  defaultSize: 240,
  minSize: 160,
  maxSize: 360,
  collapsible: true,
  snaps: [200, 280],
})
</script>

<template>
  <Card :height="240" variant="muted" :width="500">
    <Layout height="fill">
      <template #start>
        <LayoutPanel :has-divider="false" :width="sidebar.size">
          <Text v-if="!sidebar.isCollapsed" color="secondary">
            {{ Math.round(sidebar.size) }}px wide
          </Text>
        </LayoutPanel>
        <ResizeHandle
          direction="horizontal"
          has-divider
          label="Resize sidebar"
          :resizable="sidebar.props"
        />
      </template>
      <LayoutContent>
        <VStack :gap="2">
          <Text color="secondary">
            Drag the handle — it snaps at 200px and 280px. Drag all the way left to collapse the sidebar.
          </Text>
          <Button
            v-if="sidebar.isCollapsed"
            label="Expand sidebar"
            size="sm"
            variant="secondary"
            @click="sidebar.expand()"
          />
        </VStack>
      </LayoutContent>
    </Layout>
  </Card>
</template>
```

## 参数

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `defaultSize` | `ResizableSize \| string` | `250` | 初始尺寸。数字、精确的 `"Npx"`、以及 `pixel(value)` 都按像素算。精确的 `"N%"` 不带额外像素限制。`percent(value, { min: pixel(value) })` 或 `percent(value, { max: pixel(value) })` 会再加上一个像素下限或上限。百分比只解析一次成像素：给了 `containerRef` 就按它算，否则按视口算，之后不再跟着基准变化。 |
| `minSize` | `ResizableSize \| string` | `50` | 最小尺寸。数字、精确的 `"Npx"`、`pixel(value)` 仍按像素；精确的 `"N%"` 不带额外像素限制；`percent()` 会加且只加一个限制。百分比下限在基准变化时会重新解析，并把当前像素值夹回范围内。 |
| `maxSize` | `ResizableSize \| string` | `Infinity` | 最大尺寸。数字、精确的 `"Npx"`、`pixel(value)` 仍按像素；精确的 `"N%"` 不带额外像素限制；`percent()` 会加且只加一个限制。百分比上限在基准变化时会重新解析，并把当前像素值夹回范围内。 |
| `containerRef` | `Ref` | — | 百分比按谁算。这个 ref 由调用方提供，hook 不会自己去猜。不传就按视口算。ref 指的元素以后可以换，基准跟着换。目标元素还没完成布局时（没挂载、`display:none`、已脱离文档），百分比先按临时的 1200px 算，也不会写进持久化。 |
| `direction` | `'horizontal' \| 'vertical'` | `'horizontal'` | 这个区域沿哪个方向改尺寸。它决定百分比的基准取容器的行内还是块方向内容盒尺寸，也必须和交给 `ResizeHandle` 的方向一致。 |
| `collapsible` | `boolean` | `false` | 拖到收起阈值以下时，是否把区域收成 0。 |
| `collapsedSize` | `number` | `40` | 可收起的区域低于这个像素阈值就吸到 0。 |
| `snaps` | `number[]` | `[]` | 尺寸只会停在这些像素点上。离两个点一样远时，留在更靠前的那个。 |
| `autoSaveId` | `string` | — | 把尺寸和收起状态存进 localStorage 的键名，实际键名前面带 `astryx-resizable:`。 |
| `defaultIsCollapsed` | `boolean` | `false` | 初始收起状态（非受控）。localStorage 里有记录时以记录为准。 |
| `isCollapsed` | `boolean` | — | 受控的收起状态。传了它，`collapse()`、`expand()` 和拖过阈值都不再自己改状态，而是通过 `onCollapseChange` 报给你。 |
| `onSizeChange` | `(size: number) => void` | — | 每次拖动、调整或收起之后，带上解析好的像素尺寸调用。 |
| `onCollapseChange` | `(isCollapsed: boolean) => void` | — | 收起状态每变一次调用一次，拖动或代码触发都算。 |

传 <code>regions</code> 走多区域模式时，参数是 <code>regions</code>（键到 <code>ResizableRegionConfig</code> 的映射）加上 <code>direction</code>、<code>containerRef</code>、<code>autoSaveId</code>。每个区域除了上面这些尺寸、收起和吸附字段，还能给 <code>shrinkOrder</code>：容器装不下时，数字小的先让出空间。

## 返回值

| 名称 | 类型 | 说明 |
| --- | --- | --- |
| `size` | `number` | 当前尺寸，单位像素。响应式。 |
| `isCollapsed` | `boolean` | 区域当前是否已收起。响应式。 |
| `collapse` | `() => void` | 用代码把区域收起来。 |
| `expand` | `() => void` | 从收起状态展开。 |
| `resize` | `(size: number) => void` | 调到指定的像素尺寸。 |
| `props` | `ResizableProps` | 展开给可调整的组件，或者交给 `ResizeHandle`。 |

传 <code>regions</code> 时返回 <code>Record&lt;string, ResizableRegion&gt;</code>：每个键对应一个形状相同的对象。
