# Bottom Sheet Switcher

## Usage

Coordinates a multi-step bottom-sheet flow in one shared dialog; bind `v-model:active-sheet` to a nested `BottomSheet`'s `sheet-id` to open or switch steps, and to `null` to close.

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

const activeSheet = ref<string | null>(null)
</script>

<template>
  <button type="button" @click="activeSheet = 'details'">
    Start checkout
  </button>
  <BottomSheetSwitcher v-model:active-sheet="activeSheet">
    <BottomSheet sheet-id="details" label="Details">
      <p>Confirm the order.</p>
    </BottomSheet>
    <BottomSheet sheet-id="review" label="Review">
      <p>Check everything before you pay.</p>
    </BottomSheet>
  </BottomSheetSwitcher>
</template>
```

Every child needs its own `sheet-id` and a non-empty `label`. The switcher names the dialog from its own `label`, falling back to the active sheet's label, so the flow keeps one accessible name the whole way through. Only the sheet whose `sheet-id` matches `active-sheet` is interactive; during a handoff the previous panel stays painted underneath, inert, until the new step has settled, so changing steps does not flash the scrim.

`purpose` sets how the whole flow can be dismissed, matching `Dialog`. Leave it out and the step that is showing decides: `info` allows Escape, a scrim click, and a swipe, `form` allows Escape only so typed input is not thrown away, and `required` blocks every implicit path. `hasScrim` defaults to `true` and draws one scrim for the entire flow; it stays up while steps change and only goes away when the flow closes, which is what makes a multi-step flow feel like one surface instead of several dialogs.

Escape asks the switcher to close. `cancel` fires first with the native cancel event, so calling `event.preventDefault()` on it keeps the flow open.

```ts
import { BottomSheet, BottomSheetSwitcher } from '@astryx-vue/core/BottomSheet'
```

### Anatomy

- **Shared dialog**: One native dialog that owns modality, focus, dismissal, and lifecycle for the complete flow.
- **Sheet panels**: Direct `BottomSheet` children; exactly one is interactive while a previous panel may remain visible and inert during a handoff.
- **Scrim**: Native dialog backdrop shown by the default scrim-backed modal presentation.

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Use when each step depends on the previous one and only one step needs attention at a time. |
| Do | Give every child a unique `sheet-id` and non-empty `label`, choose its `purpose` to match dismissal requirements, and follow the [WAI-ARIA Dialog (Modal) pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) for scrim-backed flows. |
| Don't | Don't split information across sheets when people need to compare it; use a full-page layout that keeps the relevant content visible together instead. |
| Don't | Don't use the switcher when multiple panels must stay interactive or visible together; `active-sheet` intentionally selects one interactive step. |

## Examples

Common configurations, variations, and states.

### Review flow

A two-step form flow that lets a person review settings, confirm them, or move back without replacing the shared dialog.

```vue
<script setup lang="ts">
import { BottomSheet, BottomSheetSwitcher } from '@astryx-vue/core/BottomSheet'
import { Button } from '@astryx-vue/core/Button'
import { Heading } from '@astryx-vue/core/Heading'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { shallowRef } from 'vue'

const activeSheet = shallowRef<string | null>(null)
</script>

<template>
  <Button label="Review settings" variant="secondary" @click="activeSheet = 'review'" />
  <BottomSheetSwitcher v-model:active-sheet="activeSheet" label="Review settings">
    <BottomSheet sheet-id="review" label="Review settings" height="hug">
      <VStack :gap="4">
        <VStack :gap="1">
          <Heading :level="3">
            Review settings
          </Heading>
          <Text color="secondary" type="supporting">
            Daily summaries will be sent by email.
          </Text>
        </VStack>
        <HStack :gap="2">
          <Button label="Cancel" variant="secondary" @click="activeSheet = null" />
          <Button label="Continue" variant="secondary" @click="activeSheet = 'confirm'" />
        </HStack>
      </VStack>
    </BottomSheet>

    <BottomSheet sheet-id="confirm" label="Confirm settings" height="hug">
      <VStack :gap="4">
        <VStack :gap="1">
          <Heading :level="3">
            Confirm settings
          </Heading>
          <Text color="secondary" type="supporting">
            Your notification settings are ready to save.
          </Text>
        </VStack>
        <HStack :gap="2">
          <Button label="Back" variant="secondary" @click="activeSheet = 'review'" />
          <Button label="Save" variant="secondary" @click="activeSheet = null" />
        </HStack>
      </VStack>
    </BottomSheet>
  </BottomSheetSwitcher>
</template>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `activeSheet` | `string \| null` | `null` | Bound with `v-model:active-sheet`. The `sheet-id` of the step to show. Bind it to `null` to close the whole flow. |
| `label` | `string` | — | Accessible name for the whole flow. When omitted, the active sheet's own `label` is used, and `Bottom sheet` when there is none. |
| `purpose` | `'info' \| 'form' \| 'required'` | active sheet's `purpose` | How the whole flow may be dismissed. `info` allows Escape, a scrim click, and a swipe; `form` allows Escape only; `required` blocks every implicit path. |
| `hasScrim` | `boolean` | `true` | Draws one scrim for the entire flow. It stays up while steps change and only goes away when the flow closes. Without it the page behind stays interactive and scrolling. |

## Events

- `update:activeSheet(value: string | null)`: fires with the step the switcher wants to show, or `null` to close. This is the `v-model:active-sheet` binding.
- `cancel(event: Event)`: fires when the dialog receives a native cancel request, before the flow closes. Call `event.preventDefault()` on the event to keep it open.

## Slots

- `default`: the `BottomSheet` steps. Each one needs a unique `sheet-id` and its own `label`.
