# Responsive web app

One web app for desktop, tablet and phone browsers. Developers build it once with `AppShell`; the shell and its components rearrange themselves by the width they're given, so there is no phone variant to build or keep in sync. Feature **VI-017** · PRODUCT. (The native mobile app is a separate surface with its own React Native layout.)

## Three layouts

| Width of the shell | Layout | Vault | Actions | Chat dock | Give feedback | Peeks, Go deeper, Sources |
| --- | --- | --- | --- | --- | --- | --- |
| **≥ 1024** | `wide` | Docked left, open | Toolbar top-right, with labels | Bottom-center, clear of the Vault | Floating bottom-right | Card; side panel on the right |
| **720 – 1023** | `medium` | Drawer over the map, closed (☰ top-left) | Toolbar, icons only (counts as badges) | Bottom-center | Floating bottom-right | Card; sheet below 900px |
| **< 720** | `compact` | Bottom sheet (☰ in the top bar) | Top bar: ☰ · title · Edit · Sources · ⋯ (Export, Help) | Full width; hidden while a sheet or the editing bar is up | In the Vault footer | Bottom sheets |

The layout follows the **shell's own width** (a ResizeObserver), not the screen — a split-screen window, an embedded frame or the docs preview all get the right layout. Touch behaviour is separate and follows the pointer in use (`VisualMap interaction="auto"`): an iPad in landscape gets the wide layout with tap-to-peek.

## Building it

```tsx
import { AppShell, Vault, VisualMap, ChatTurn, Composer, exportItems } from '@visualli/design-system/components';

<AppShell
  title={doc.title}
  vault={<Vault entries={entries} activeId={id} onOpen={open} onNew={create} onExport={exportAs} user={user} onComfort={showComfort} />}
  actions={[
    { id: 'edit', label: 'Edit', icon: 'pencil', pressed: editing, pinned: true, onClick: toggleEdit },
    { id: 'sources', label: 'Sources', icon: 'paperclip', count: sources.length, pressed: showSources, pinned: true, onClick: toggleSources },
    { id: 'export', label: 'Export', icon: 'download', menu: exportItems(exportCurrent) },
    { id: 'help', label: 'Help', icon: 'help-circle', quiet: true, onClick: openHelp },
  ]}
  map={<VisualMap doc={doc} editing={editing} onEditingChange={setEditing} aside={sourcesAside} … />}
  dock={<>{turns.map((t) => <ChatTurn key={t.id} {...t} />)}<Composer value={draft} onChange={setDraft} onSend={send} /></>}
  onFeedback={() => window.Quackback?.('open')}
>
  {/* dialogs: Comfort & appearance … */}
</AppShell>
```

What AppShell does for you:
- **Vault:** wires *Hide vault*, closes the drawer or sheet after *Open* and *New Visualli*, closes on Escape or a tap on the scrim, and moves *Give feedback* into the Vault footer on phones.
- **Actions:** one list. `pinned` actions (at most two) stay in the phone top bar; the rest, and `menu` items under their label, go into ⋯. `quiet` actions (Help) are icon-only after a divider. `count` shows as a pill on wide and a badge elsewhere.
- **Map:** sets `insets` (room for the Vault, top bar and dock) and fills the shell; on phones the zoom controls sit under the top bar, opposite the depth trail.
- **Room for panels:** while a side panel is docked, the toolbar, zoom and dock slide left of it and *Give feedback* hides; while a sheet or the phone editing bar is up, the dock steps aside.
- **Phone browsers:** `height: 100dvh` (the composer never hides under the address bar), safe-area padding for the notch and home bar, and a 16px composer so iOS doesn't zoom on focus.

## On the page

Add these to the web app's `index.html` so it behaves like an app on phones:

```html
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#f6f3ee" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0f1220" media="(prefers-color-scheme: dark)">
```

`viewport-fit=cover` turns on the safe-area insets AppShell already uses. Don't set `maximum-scale` or `user-scalable=no` — people who zoom the page need to.

## Rules
- Every feature stays reachable at every width; nothing is desktop-only. If an action doesn't fit, it moves (⋯, Vault footer), it doesn't disappear.
- Touch targets are at least 44px on phones (the top-bar buttons are 44px).
- Force a layout only to test it: `<AppShell layout="compact">`. In the docs, the Web app page's **Desktop / Tablet / Phone** toggle renders the prototype at real widths.
- Breakpoints live in `SHELL_BREAKPOINTS` (`{ medium: 720, wide: 1024 }`); `shellLayoutFor(width)` gives the layout for a width.
