# Dock

> The page's drawers: the page in the middle, a drawer docked on the right (the record a row opened, an inspector) and one along the bottom (a log, a console, a timeline), each with a title and a close, each resizable by its edge and remembered. The page decides when each is open; on a phone the right drawer becomes a sheet and the bottom one follows the page.

<!-- id: ui-dock · area: ui · stability: stable · html: https://osysharp.com/reference/ui/dock/ -->

## Summary        {#summary}

`Dock` arranges a page around what it has SELECTED: the list or table stays in the middle, the thing a person opened
docks on the right without covering it, and an ambient stream — a log, a console, a timeline — runs along the bottom.
Both drawers are opened by the page (it is what knows what is selected), closed by their own ×, and resized by
dragging their edge or with the arrow keys; the size is remembered.

```osy title="a table with the opened row docked on the right and a log below" test app=ui-dock
class Job { public string Ref; public string Title; public string State; }

List<Job> Jobs() {
  return [
    new Job { Ref = "J-104", Title = "Rebuild the search index", State = "Running" },
    new Job { Ref = "J-103", Title = "Rotate the signing keys", State = "Queued" },
  ];
}

[Route("/jobs")] [Render(CSR)] [AllowAnonymous]
component JobsPage() {
  string open = "";
  bool log = true;
  action Open(Job j) { open = j.Ref; }
  action Close() { open = ""; }
  action CloseLog() { log = false; }

  render {
    Box(h: Length.Screen) {
      Dock(right: open != "", rightTitle: "Job " + open, onCloseRight: Close,
           bottom: log, bottomTitle: "Log", onCloseBottom: CloseLog) {
        DataGrid(label: "Jobs", rows: Jobs(), rowSelected: Open, columns: [
          new GridColumn<Job> { Name = "Ref", Label = "Job", Value = j => j.Ref, Width = 90 },
          new GridColumn<Job> { Name = "Title", Label = "Title", Value = j => j.Title, Fill = true },
          new GridColumn<Job> { Name = "State", Label = "State", Value = j => j.State, Width = 110 },
        ]);
        slot right { Stack(gap: 2, p: 4) { Text(Jobs().First(j => j.Ref == open).Title, fontWeight: FontWeight.Semibold); } }
        slot bottom { Text("10:24:07  index shard 3 of 8 rebuilt", fontFamily: Font.Mono, fontSize: FontSize.Code); }
      }
    }
  }
}
```

## Signature      {#signature}

```osy syntax
Dock(bool right = false, bool bottom = false, string rightTitle = "Details", string bottomTitle = "Panel",
     Action onCloseRight = null, Action onCloseBottom = null) { <the page>; slot right { … } slot bottom { … } }
```

## Description    {#description}

### Who opens a drawer?   {#who-opens}

The page. `right:` and `bottom:` are plain booleans the page computes — `open != null`, a toggle in its toolbar — and
`onCloseRight` / `onCloseBottom` are what the drawers' × buttons run. That keeps the decision where the knowledge is:
the page knows which row is selected; the dock only knows how to show it.

### How big is it, and does it stay that size?   {#size}

A right drawer starts 380px wide and a bottom one 240px tall. Each edge is a handle: drag it, or focus it and use the
arrow keys (Escape puts it back where the drag started). The size is `[Remembered]`, so it survives a reload and the
next visit, per browser.

### What happens on a phone?   {#narrow}

Below `Laptop` the two drawers part ways. The right one is what the person OPENED, so it takes the screen as a sheet
(its own close, and Escape) and closes back to the list. The bottom one is ambient, so it follows the page in the
flow under its title instead of burying the page the moment it opens.

### Where does the page scroll?   {#scroll}

Give `Dock` the page's height — `WorkbenchShell` does, as does any shell with `Panes = true`, or a `Box(h:
Length.Screen)` — and the middle scrolls on its own while the drawers stay put. Each drawer scrolls inside itself.

### How do I drive it in a test?   {#test}

The drawers are regions named by their titles: `Assert.Visible("Rebuild the search index", within: "Job J-104")`,
and their close is `Ui.Click("Close Job J-104")`. Give each drawer a title that differs from anything in the table —
"Job J-104" rather than "J-104" — or a scope naming it matches the row too.

## See also       {#see-also}

- [WorkbenchShell](https://osysharp.com/reference/ui/shell-workbench/) — the shell built for a page of drawers
- [Save, discard, undo — three ways to show a page's unit of work](https://osysharp.com/reference/ui/saving/) — `SaveActions`, the save that belongs in a drawer's head
- [Arrangements — five ways to build a whole app, and which look wears each](https://osysharp.com/reference/ui/arrangements/) — the record desk and the workbench, both built on `Dock`
- [drag](https://osysharp.com/reference/ui/drag/) — the gesture the edges use
