# App Shell

## Usage

`AppShell` is the page frame. The default slot is the page content. `topNav`, `sideNav`, `banner`, and `mobileNav` are named slots.

```vue
<script setup lang="ts">
import { AppShell } from '@astryx-vue/core/AppShell'
import { SideNav, SideNavItem } from '@astryx-vue/core/SideNav'
</script>

<template>
  <AppShell :content-padding="6">
    <template #sideNav>
      <SideNav>
        <SideNavItem href="#orders" is-selected label="Orders" />
      </SideNav>
    </template>
    <p>Orders</p>
  </AppShell>
</template>
```

`variant` is `wash`, `surface`, `section`, or `elevated` (the default). `contentPadding` is a spacing step for the content region. `height` is `fill` or `auto`. `fill` uses `100dvh`; an inline `height: 100%` overrides it when the shell sits in a sized preview.

`mobileNav` is `false` or `{ breakpoint, isOpen, hasToggle }`. The default breakpoint is `md` (768px). Below the breakpoint the side nav moves into a drawer. A top nav inside the shell uses that drawer instead of opening one of its own. `v-model:mobile-nav-open` controls the drawer, and the component emits `mobileNavOpenChange`.

A skip link points at `#astryx-app-shell-main`. The main region is a `div` with `role="main"` and `tabindex="-1"`. With `variant="elevated"` and both a top nav and a side nav on a wide viewport, the content sits on a rounded backdrop.

`useAppShellMobile` reads the nearest shell. Outside a shell, or when mobile nav is off, `isMobile` and `isMobileNavEnabled` are false and the open and close methods do nothing.

### Anatomy

- **Page shell**: Outermost application frame that owns page-level navigation, responsive shell behavior, and the main content landmark.
- **Skip link**: First focusable element on the page. Visually hidden until focused, then moves focus to the main content area.
- **Banner**: The banner slot, for system-wide announcements. Renders above the top nav, inside the banner landmark.
- **Top navigation**: The topNav slot, typically TopNav. Below the mobile breakpoint it becomes a compact bar carrying the nav toggle.
- **Side navigation**: The sideNav slot, typically SideNav. Inline above the breakpoint, moved into the mobile drawer below it.
- **Main content**: The default slot, rendered in the main landmark. Scrolls internally when height is fill, and with the page when it is auto.
- **Mobile nav drawer**: Generated below the breakpoint from the nav slots unless mobileNav disables or replaces it. A modal dialog: it traps focus and returns focus to the toggle on close.

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Put application navigation in the topNav and sideNav slots. Put page regions in Layout. |
| Do | Give the side nav a heading so the mobile drawer has a name. |
| Don't | Open a second drawer from TopNav while it is inside an AppShell. The shell already owns that drawer. |
| Don't | Call useAppShellMobile and assume a shell is present. Check isMobileNavEnabled. |

## Examples

### Content only

A shell with no navigation, for a full-bleed page, an auth screen, or an embedded view.

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

<template>
  <AppShell :content-padding="6" style="height: 100%; min-height: 0">
    <VStack :gap="4">
      <p class="layout-title layout-title--page">
        Page Content
      </p>
      <Text type="body">
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
      </Text>
    </VStack>
  </AppShell>
</template>
```

### Side nav only

The side nav heading carries the app identity, so a top nav is unnecessary.

```vue
<script setup lang="ts">
import { AppShell } from '@astryx-vue/core/AppShell'
import { SideNav, SideNavHeading, SideNavItem, SideNavSection } from '@astryx-vue/core/SideNav'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
</script>

<template>
  <AppShell :content-padding="6" style="height: 100%; min-height: 0">
    <template #sideNav>
      <SideNav>
        <template #header>
          <SideNavHeading heading="App Shell" heading-href="#">
            <template #icon>
              <span class="i-carbon-cube nav-mark" />
            </template>
          </SideNavHeading>
        </template>
        <SideNavSection is-header-hidden title="Main">
          <SideNavItem href="#" icon-class="i-carbon-home" is-selected label="Dashboard" />
          <SideNavItem href="#" icon-class="i-carbon-analytics" label="Analytics" />
          <SideNavItem href="#" icon-class="i-carbon-folder" label="Projects" />
        </SideNavSection>
        <SideNavSection title="Organization">
          <SideNavItem href="#" icon-class="i-carbon-user" label="Team" />
          <SideNavItem href="#" icon-class="i-carbon-settings" label="Settings" />
        </SideNavSection>
      </SideNav>
    </template>
    <VStack :gap="4">
      <p class="layout-title layout-title--page">
        Page Content
      </p>
      <Text type="body">
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
      </Text>
    </VStack>
  </AppShell>
</template>
```

### Top nav only

A top nav and no side navigation, for a landing page.

```vue
<script setup lang="ts">
import { AppShell } from '@astryx-vue/core/AppShell'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { TopNav, TopNavHeading, TopNavItem } from '@astryx-vue/core/TopNav'
</script>

<template>
  <AppShell :content-padding="6" style="height: 100%; min-height: 0">
    <template #topNav>
      <TopNav label="Main navigation">
        <template #heading>
          <TopNavHeading heading="App Shell">
            <template #icon>
              <span class="i-carbon-cube nav-mark" />
            </template>
          </TopNavHeading>
        </template>
        <template #start>
          <TopNavItem href="#" is-selected label="Home" />
          <TopNavItem href="#" label="Products" />
          <TopNavItem href="#" label="Docs" />
        </template>
      </TopNav>
    </template>
    <VStack :gap="4">
      <p class="layout-title layout-title--page">
        Page Content
      </p>
      <Text type="body">
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
      </Text>
    </VStack>
  </AppShell>
</template>
```

### Top nav with side nav

The usual frame: a top nav for the app, and a side nav for the pages.

```vue
<script setup lang="ts">
import { AppShell } from '@astryx-vue/core/AppShell'
import { SideNav, SideNavItem, SideNavSection } from '@astryx-vue/core/SideNav'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { TopNav, TopNavHeading, TopNavItem } from '@astryx-vue/core/TopNav'
</script>

<template>
  <AppShell :content-padding="6" style="height: 100%; min-height: 0">
    <template #topNav>
      <TopNav label="Main navigation">
        <template #heading>
          <TopNavHeading heading="App Shell">
            <template #icon>
              <span class="i-carbon-cube nav-mark" />
            </template>
          </TopNavHeading>
        </template>
        <template #start>
          <TopNavItem href="#" is-selected label="Home" />
          <TopNavItem href="#" label="Products" />
          <TopNavItem href="#" label="Docs" />
        </template>
      </TopNav>
    </template>
    <template #sideNav>
      <SideNav>
        <SideNavSection is-header-hidden title="Main">
          <SideNavItem href="#" icon-class="i-carbon-home" is-selected label="Dashboard" />
          <SideNavItem href="#" icon-class="i-carbon-analytics" label="Analytics" />
          <SideNavItem href="#" icon-class="i-carbon-folder" label="Projects" />
        </SideNavSection>
        <SideNavSection title="Organization">
          <SideNavItem href="#" icon-class="i-carbon-user" label="Team" />
          <SideNavItem href="#" icon-class="i-carbon-settings" label="Settings" />
        </SideNavSection>
      </SideNav>
    </template>
    <VStack :gap="4">
      <p class="layout-title layout-title--page">
        Page Content
      </p>
      <Text type="body">
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
      </Text>
    </VStack>
  </AppShell>
</template>
```

### With banner

Top nav, side nav, and a dismissable info banner between the navigation and the content.

```vue
<script setup lang="ts">
import { AppShell } from '@astryx-vue/core/AppShell'
import { Banner } from '@astryx-vue/core/Banner'
import { SideNav, SideNavItem, SideNavSection } from '@astryx-vue/core/SideNav'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { TopNav, TopNavHeading, TopNavItem } from '@astryx-vue/core/TopNav'
</script>

<template>
  <AppShell :content-padding="6" style="height: 100%; min-height: 0">
    <template #topNav>
      <TopNav label="Main navigation">
        <template #heading>
          <TopNavHeading heading="App Shell">
            <template #icon>
              <span class="i-carbon-cube nav-mark" />
            </template>
          </TopNavHeading>
        </template>
        <template #start>
          <TopNavItem href="#" is-selected label="Home" />
          <TopNavItem href="#" label="Products" />
          <TopNavItem href="#" label="Docs" />
        </template>
      </TopNav>
    </template>
    <template #sideNav>
      <SideNav>
        <SideNavSection is-header-hidden title="Main">
          <SideNavItem href="#" icon-class="i-carbon-home" is-selected label="Dashboard" />
          <SideNavItem href="#" icon-class="i-carbon-analytics" label="Analytics" />
          <SideNavItem href="#" icon-class="i-carbon-folder" label="Projects" />
        </SideNavSection>
        <SideNavSection title="Organization">
          <SideNavItem href="#" icon-class="i-carbon-user" label="Team" />
          <SideNavItem href="#" icon-class="i-carbon-settings" label="Settings" />
        </SideNavSection>
      </SideNav>
    </template>
    <template #banner>
      <Banner
        container="section"
        description="The system will undergo maintenance tonight at 10pm UTC."
        is-dismissable
        status="info"
        title="System maintenance scheduled"
      />
    </template>
    <VStack :gap="4">
      <p class="layout-title layout-title--page">
        Page Content
      </p>
      <Text type="body">
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
      </Text>
    </VStack>
  </AppShell>
</template>
```

## App Shell Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `'wash' \| 'surface' \| 'section' \| 'elevated'` | `'elevated'` | Page background. `elevated` with both a top nav and a side nav, on a wide viewport, puts the content on a rounded backdrop. |
| `contentPadding` | `SpacingStep` | - | Padding of the content region. Omit it and the content is flush. |
| `height` | `'fill' \| 'auto'` | `'fill'` | `fill` uses `100dvh`. An inline height overrides that length. |
| `mobileNav` | `false \| AppShellMobileNav` | - | `false` turns the drawer off. An object sets `breakpoint` (`sm` 640, `md` 768, `lg` 1024, `xl` 1280, `2xl` 1536, or `none`), `isOpen`, and `hasToggle`. |
| `mobileNavOpen` | `boolean` | `false` | `v-model:mobile-nav-open`. The component also emits `mobileNavOpenChange`. |

`SpacingStep` accepts `0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10`. The default breakpoint is <code>md</code>, and the query is <code>(width &lt; Npx)</code>. <code>hasToggle</code> defaults to true.

## Slots

| Name | Description |
| --- | --- |
| `default` | Page content, rendered in the main region. |
| `topNav` | The top bar, usually a `TopNav`. On a narrow viewport its items move into the shell drawer. |
| `sideNav` | The side bar, usually a `SideNav`. Below the breakpoint it renders inside the drawer instead of beside the content. |
| `banner` | A banner between the navigation and the content. |
| `mobileNav` | Replaces the default drawer. |

## useAppShellMobile

Import it from <code>@astryx-vue/core</code>. It returns the nearest shell:

| Name | Type | Description |
| --- | --- | --- |
| `isMobile` | `boolean` | True when the viewport is below the shell breakpoint. |
| `isMobileNavOpen` | `boolean` | Whether the drawer is open. |
| `isMobileNavEnabled` | `boolean` | False outside a shell, or when `mobileNav` is false. |
| `hasAutoToggle` | `boolean` | Whether the shell draws the menu button. |
| `hasSideNav` | `boolean` | Whether the side nav slot is set. |
| `mobileNavId` | `string` | Id of the drawer. Top nav items teleport into `#${id}-items`. |
| `toggleMobileNav` | `() => void` | Toggles the drawer. A no-op outside a shell. |
| `openMobileNav` | `() => void` | Opens the drawer. |
| `closeMobileNav` | `() => void` | Closes the drawer. |

The main region is <code>#astryx-app-shell-main</code>, a <code>div</code> with <code>role="main"</code> and <code>tabindex="-1"</code>. The skip link label follows the active locale.

## Native attributes

`AppShell` renders a `<div>`.

The following go to that element:

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

`class` is merged. The root sets `data-variant` and `data-height`.
