# Layout

## Usage

`Layout` arranges up to five regions: header, start, content, end, and footer. The default slot is the content. The other regions are named slots. `AppShell` owns the page frame; use `Layout` inside a page, a card, or a dialog. Use `HStack` or `VStack` when the content is only a row or a column.

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

<template>
  <Layout>
    <template #header>
      <LayoutHeader has-divider>
        Orders
      </LayoutHeader>
    </template>
    <LayoutContent>
      Select an order.
    </LayoutContent>
  </Layout>
</template>
```

`height` is `fill` or `auto`. `fill` stretches to the container and scrolls the content; `auto` grows with the content. `contentWidth` limits the width of the content and centers it. Dividers stay full width. A percentage, `fit-content`, `max-content`, `min-content`, or a bare `var()` uses the constrained width mode. `padding` sets this layout's own outer inset and does not pass to a nested layout. `defaultHasDividers` is the fallback for a header or footer that does not set `hasDivider`.

`LayoutHeader`, `LayoutFooter`, `LayoutContent`, and `LayoutPanel` are the region components. A panel's `width` is pixels when it is a number. Pass `resizable` from `useResizable` when a `ResizeHandle` controls that width.

### Anatomy

- **Layout container**: General layout primitive that places the header, start, content, end, and footer slots.
- **Header**: Optional LayoutHeader region supplied by the caller, typically in the header slot.
- **Panel**: Optional LayoutPanel region supplied by the caller in the start or end slot.
- **Content area**: Optional LayoutContent region supplied by the caller in the content slot.
- **Footer**: Optional LayoutFooter region supplied by the caller, typically in the footer slot.

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Use Layout when the page or card has a header, a side panel, or a footer that should stay put while the content scrolls. |
| Do | Put the scroll on `LayoutContent`. Leave the header and footer outside it. |
| Don't | Use Layout as the application frame. Use AppShell for top and side navigation. |
| Don't | Draw a panel divider and a resize-handle divider on the same edge. |

## Examples

### Basic card

A card with a header, a scrolling content area, and a footer of actions.

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

<template>
  <Card :height="300" width="100%" style="max-width: 400px">
    <Layout>
      <template #header>
        <LayoutHeader has-divider>
          <p class="layout-title">
            Card Title
          </p>
        </LayoutHeader>
      </template>
      <LayoutContent>
        <VStack :gap="3">
          <Text type="body">
            This is a basic card layout with a header, scrollable content area, and footer. The layout automatically handles padding and spacing between sections.
          </Text>
          <Text type="body">
            When content exceeds the available height, the content area scrolls independently while the header and footer stay fixed in place.
          </Text>
          <Text type="body">
            This pattern works well for modal dialogs, detail panels, and any card where the amount of content is unpredictable.
          </Text>
          <Text type="body">
            The dividers between header, content, and footer provide clear visual boundaries between the fixed and scrollable regions.
          </Text>
        </VStack>
      </LayoutContent>
      <template #footer>
        <LayoutFooter has-divider>
          <HStack :gap="2" h-align="end">
            <Button label="Cancel" variant="secondary" />
            <Button label="Save" variant="primary" />
          </HStack>
        </LayoutFooter>
      </template>
    </Layout>
  </Card>
</template>
```

### Content only

A card that is only a content area, with no header or footer.

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

<template>
  <Card width="100%" style="max-width: 400px">
    <Layout>
      <LayoutContent>
        <VStack :gap="3">
          <p class="layout-title">
            Simple Content
          </p>
          <Text type="body">
            A layout can have just content without header or footer. This is useful for simple cards or content blocks that don't need structured sections.
          </Text>
        </VStack>
      </LayoutContent>
    </Layout>
  </Card>
</template>
```

### Content width

contentWidth constrains and centers the text while the dividers stay full bleed.

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { Layout, LayoutContent, LayoutFooter, LayoutHeader } from '@astryx-vue/core/Layout'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
</script>

<template>
  <Layout :content-width="360" height="fill">
    <template #header>
      <LayoutHeader has-divider>
        <p class="layout-title">
          Centered Form
        </p>
      </LayoutHeader>
    </template>
    <LayoutContent>
      <VStack :gap="3">
        <Text type="body">
          The contentWidth prop constrains content to a maximum width and centers it within the layout. Dividers remain full-bleed while content stays narrow and readable.
        </Text>
        <Text color="secondary" type="body">
          Common widths: 640 for forms, 960 for content pages.
        </Text>
      </VStack>
    </LayoutContent>
    <template #footer>
      <LayoutFooter has-divider>
        <HStack :gap="2" h-align="end">
          <Button label="Cancel" variant="secondary" />
          <Button label="Submit" variant="primary" />
        </HStack>
      </LayoutFooter>
    </template>
  </Layout>
</template>
```

### Dual panel

A file browser: folders at the start, files in the middle, details at the end.

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

<template>
  <Card width="100%" style="max-width: 500px">
    <Layout>
      <template #header>
        <LayoutHeader has-divider>
          <p class="layout-title">
            File Browser
          </p>
        </LayoutHeader>
      </template>
      <template #start>
        <LayoutPanel :width="120" has-divider>
          <VStack :gap="1">
            <Text color="secondary" type="label">
              Folders
            </Text>
            <List>
              <ListItem label="Documents" />
              <ListItem is-selected label="Projects" />
              <ListItem label="Downloads" />
            </List>
          </VStack>
        </LayoutPanel>
      </template>
      <LayoutContent>
        <VStack :gap="2">
          <Text color="secondary" type="label">
            Files
          </Text>
          <Card variant="muted">
            <Text type="body">
              Select a folder to view its contents
            </Text>
          </Card>
        </VStack>
      </LayoutContent>
      <template #end>
        <LayoutPanel :width="120" has-divider>
          <VStack :gap="2">
            <Text color="secondary" type="label">
              Details
            </Text>
            <Text type="body">
              Select a file to view details
            </Text>
          </VStack>
        </LayoutPanel>
      </template>
    </Layout>
  </Card>
</template>
```

### Full bleed content

Content runs edge to edge with no padding, for a table or an image.

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { Card } from '@astryx-vue/core/Card'
import { Layout, LayoutContent, LayoutFooter, LayoutHeader } from '@astryx-vue/core/Layout'
import { Section } from '@astryx-vue/core/Section'
import { HStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
</script>

<template>
  <Card width="100%" style="max-width: 400px">
    <Layout>
      <template #header>
        <LayoutHeader has-divider>
          <p class="layout-title">
            Full Bleed Example
          </p>
        </LayoutHeader>
      </template>
      <LayoutContent :padding="0">
        <Section variant="muted">
          <Text type="body">
            Section automatically escapes the parent container padding to fill edge-to-edge. Useful for wash backgrounds, tables, or images that need to span the full width.
          </Text>
        </Section>
      </LayoutContent>
      <template #footer>
        <LayoutFooter has-divider>
          <HStack :gap="2" h-align="end">
            <Button label="Close" variant="secondary" />
          </HStack>
        </LayoutFooter>
      </template>
    </Layout>
  </Card>
</template>
```

### Sidebar navigation

A settings page with a navigation panel, a content area, a header, and a footer.

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { Card } from '@astryx-vue/core/Card'
import { Layout, LayoutContent, LayoutFooter, LayoutHeader, LayoutPanel } from '@astryx-vue/core/Layout'
import { List, ListItem } from '@astryx-vue/core/List'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
</script>

<template>
  <Card width="100%" style="max-width: 500px">
    <Layout>
      <template #header>
        <LayoutHeader has-divider>
          <p class="layout-title">
            Settings
          </p>
        </LayoutHeader>
      </template>
      <template #start>
        <LayoutPanel :width="150" has-divider label="Settings sections" role="navigation">
          <List>
            <ListItem is-selected label="General" />
            <ListItem label="Account" />
            <ListItem label="Privacy" />
            <ListItem label="Notifications" />
            <ListItem label="Security" />
          </List>
        </LayoutPanel>
      </template>
      <LayoutContent>
        <VStack :gap="3">
          <p class="layout-title">
            General Settings
          </p>
          <Text type="body">
            Configure your general preferences here. The sidebar navigation allows you to switch between different settings sections.
          </Text>
        </VStack>
      </LayoutContent>
      <template #footer>
        <LayoutFooter has-divider>
          <HStack :gap="2" h-align="end">
            <Button label="Reset" variant="secondary" />
            <Button label="Save Changes" variant="primary" />
          </HStack>
        </LayoutFooter>
      </template>
    </Layout>
  </Card>
</template>
```

## Layout Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `contentWidth` | `number \| string` | - | Maximum width of the aligned content. Dividers stay full width. Numbers are pixels. A percentage, `fit-content`, `max-content`, `min-content`, or a bare `var()` uses constrained width. |
| `height` | `'fill' \| 'auto'` | `'fill'` | `fill` stretches to the container and scrolls the content. `auto` grows with the content. |
| `padding` | `SpacingStep` | - | Outer inset for this layout only. It does not pass to a nested layout. `0` is full bleed. |
| `defaultHasDividers` | `boolean` | - | Fallback for a header or footer that does not set `hasDivider`. |

`SpacingStep` accepts `0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10`.

## Layout Slots

| Name | Description |
| --- | --- |
| `default` | The content region. Put a `LayoutContent` here. |
| `header` | The top region. Put a `LayoutHeader` here. |
| `start` | The inline-start region. Put a `LayoutPanel` here, and a `ResizeHandle` beside it when the panel resizes. |
| `end` | The inline-end region. |
| `footer` | The bottom region. Put a `LayoutFooter` here. |

## Layout Header Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `hasDivider` | `boolean` | inherited | Border on the bottom edge. Omit it to use `defaultHasDividers`, which is false when the layout does not set one. |
| `height` | `number \| string` | - | Height. Numbers are pixels, strings are used as they are. |
| `padding` | `SpacingStep` | - | Padding. `0` lets the content own its inset. |
| `paddingBlockEnd` | `SpacingStep` | - | Bottom padding only, so a tab underline can meet the divider. |
| `label` | `string` | - | Accessible name when `role` is set. |
| `role` | `AriaRole` | - | ARIA landmark role. |

`LayoutHeader` has a default slot for its content.

## Layout Footer Props

`LayoutFooter` accepts <code>hasDivider</code>, <code>height</code>, <code>padding</code>, <code>label</code>, and <code>role</code> with the same meaning as the header. <code>hasDivider</code> draws the border on the top edge. It has a default slot.

## Layout Content Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `padding` | `SpacingStep` | - | Padding. `0` lets a child such as `Section` reach the edges. |
| `isScrollable` | `boolean` | `true` | Scroll this region when the content overflows. |
| `label` | `string` | - | Accessible name when `role` is set. |
| `role` | `AriaRole` | - | ARIA landmark role. |

`LayoutContent` has a default slot.

## Layout Panel Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `hasDivider` | `boolean` | `false` | Border on the edge that faces the content. Leave it off when a `ResizeHandle` draws that line. |
| `padding` | `SpacingStep` | - | Padding. Omit it and a panel without a divider collapses the margin that faces the content. |
| `isScrollable` | `boolean` | `true` | Scroll this panel when the content overflows. |
| `width` | `number \| string` | - | Width. Numbers are pixels. Ignored while `resizable` is set. |
| `resizable` | `LayoutResizable` | - | The `props` object from `useResizable`. The panel width follows `_size`, and the object also carries the region's axis as `_direction` (`'horizontal' \| 'vertical'`), which must match the adjacent `ResizeHandle`. |
| `label` | `string` | - | Accessible name when `role` is set. |
| `role` | `AriaRole` | - | ARIA landmark role, for example `navigation`. |

`LayoutPanel` has a default slot.

## Native attributes

Each piece renders a `<div>`.

The following go to its root:

- HTML attributes.
- `aria-*` attributes.
- `data-*` attributes.
- `class`.
- `style`.

`class` is merged. `Layout` sets `data-height`.
