# AppShell

> The layout of every Keston app page: the shared Header on top, the app's Sidebar on the left, the page in the middle.

- Import: `import { AppShell } from '@thekestongroup/ui'`
- Group: Layout
- Live example: https://ui.keston.co/storybook/?path=/story/components-layout-appshell--default
- Source: https://github.com/thekestongroup/ui/blob/main/src/components/AppShell/AppShell.vue

## When to use

- As the root of every app, so all apps share the same header, account menu and responsive behaviour. Each app supplies only its title, sidebar items and pages.

## When not to use

- Sign-in, error or marketing pages that have no navigation.

## Anatomy

| Part | Required | Notes |
|---|---|---|
| skip link | Yes | "Skip to content", visible only when focused. |
| header | Yes | Header: sticky, with menu button and account menu. The menu button opens the mobile menu (narrow) or collapses the sidebar (wide). |
| sidebar | Yes | Sidebar beside the page (wide) or in a left Slideover (narrow). |
| main | Yes | The page. Default slot. |

## Properties

| Property | Type | Default | Description |
|---|---|---|---|
| title | string | — | App name, in the header and as the mobile menu's title. |
| user | HeaderUser | — | Signed-in user, for the account menu. |
| userActions | HeaderUserAction[] | all | Account menu actions. |
| signIn | boolean | true | Show Sign in when signed out. |
| items / sections | SidebarItem[] / SidebarSection[] | [] | Sidebar contents. |
| collapsible | boolean | true | Wide screens: the header's menu button collapses the sidebar. |
| collapsed | boolean (v-model) | false | Sidebar collapsed to icons. |
| menuOpen | boolean (v-model) | false | Mobile menu open. |
| linkComponent | string \| Component | "a" | For sidebar items with `to`. |
| header / sidebar | object | — | Other Header / Sidebar props. |

Events: `account-settings`, `switch-account`, `log-out`, `sign-in`.

## Spacing and layout

- Breakpoint: lg (64rem). Below it the sidebar moves into a Slideover (max width spacing 72).
- The page scrolls, not an inner box: the header sticks at the top, the sidebar sticks below it at full remaining height.
- Page padding is the page's own (e.g. `ui.main: 'p-6'`).

## Behaviour

- Narrow: menu button opens the menu. Choosing an item, or the screen becoming wide, closes it.
- Wide: sidebar collapse state is kept by `v-model:collapsed` (the app can store it).

## Accessibility

- Landmarks: header (banner), nav (inside the sidebar), main.
- Skip link moves focus to main.
- Mobile menu is a dialog titled with the app name.

## Platform adaptation

| Platform | Implementation | Differences |
|---|---|---|
| Web / desktop (Vue) | `AppShell.vue` | — |
| iOS (SwiftUI) | — | NavigationSplitView (iPad), TabView (iPhone). |
| Android (Compose) | — | Scaffold with TopAppBar and NavigationDrawer / Rail. |

## Tokens used

| Token | Used for |
|---|---|
| color background | page background |

## Do / Don't

| Do | Don't |
|---|---|
| Use one AppShell at the root of each app. | Rebuild the header per app. Use slots for app-specific additions. |

## API (from the source)

The component's exact props, model, events, slots and types, with their documentation comments.

```ts
// AppShell.vue
/**
 * The layout of a whole app page: the Header across the top, the app's
 * Sidebar down the left, and the page in the middle. Every app uses this, so
 * the header looks and behaves the same everywhere; only the sidebar's items
 * and the page change.
 *
 * - Wide screens (lg and up): the sidebar sits beside the page, and the
 *   header's menu button collapses it to icons and back (unless
 *   `collapsible` is false).
 * - Narrow screens: the sidebar is hidden and the menu button opens it in a
 *   Slideover from the left, whose title bar is the header's height. Choosing
 *   an item, or the screen becoming wide, closes it.
 * - The page itself scrolls (not an inner box), so the header stays at the
 *   top (sticky), the sidebar stays put below it, and mobile browsers can
 *   hide their address bar while scrolling.
 *
 * The account menu (top right) is the Header's: pass `user` and listen for
 * account-settings, switch-account, log-out (and sign-in when signed out).
 *
 * Slots: default (the page), header-leading, header-middle, header-trailing,
 * account, account-menu (passed to Header), sidebar-header and sidebar-footer (passed to Sidebar;
 * sidebar-header is only shown on wide screens, as the Slideover has its own
 * title).
 */
export interface AppShellProps {
  /** Name of the app, shown in the header and as the mobile menu's title. */
  title?: string
  /** The signed-in user, for the account menu. Without it, Sign in is shown. */
  user?: HeaderUser
  /** Which actions the account menu offers. Default all three. */
  userActions?: HeaderUserAction[]
  /** Show Sign in when there is no user. */
  signIn?: boolean
  /** Sidebar items in a single section. */
  items?: SidebarItem[]
  /** Sidebar items in labelled sections. */
  sections?: SidebarProps['sections']
  /** Wide screens: show the header's menu button, which collapses the sidebar to icons. */
  collapsible?: boolean
  /** Component used for sidebar items with `to`, e.g. RouterLink. */
  linkComponent?: SidebarProps['linkComponent']
  /** Other Header props, e.g. `{ ui: { title: '…' } }`. */
  header?: Omit<HeaderProps, 'title' | 'menu' | 'menuOpen' | 'sticky' | 'user' | 'userActions' | 'signIn'>
  /** Other Sidebar props, e.g. `{ label: 'Admissions' }`. */
  sidebar?: Omit<SidebarProps, 'items' | 'sections' | 'collapsible' | 'linkComponent'>
  class?: HTMLAttributes['class']
  /** Extra classes for the parts. */
  ui?: {
    main?: HTMLAttributes['class']
  }
}

const emit = defineEmits<{
  'account-settings': []
  'switch-account': []
  'log-out': []
  'sign-in': []
}>()

defineSlots<{
  default?(): unknown
  'header-leading'?(): unknown
  'header-middle'?(): unknown
  'header-trailing'?(): unknown
  account?(): unknown
  'account-menu'?(props: { close: () => void }): unknown
  'sidebar-header'?(props: { collapsed: boolean }): unknown
  'sidebar-footer'?(props: { collapsed: boolean; mobile: boolean }): unknown
}>()

/** Sidebar collapsed to icons (wide screens). Use v-model:collapsed to keep it, e.g. in localStorage. */
const collapsed = defineModel<boolean>('collapsed', { default: false })

/** Mobile menu open. Optional: use v-model:menu-open to control it. */
const menuOpen = defineModel<boolean>('menuOpen', { default: false })
```
