# Arrangements — five ways to build a whole app, and which look wears each

> A look decides how an app's pixels read; an ARRANGEMENT decides how the app is built — where navigation lives, how a record opens, where a person acts on a selection, how a change is kept, and where an assistant appears. The kit ships five archetypes, one paired with each look by default: the document desk (Ledger), the workbench (Relay), the flow (Civic), the record desk (Studio) and the command centre (Signal). Every one is ordinary kit components, so any app takes any of them in any look.

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

## Summary        {#summary}

Two apps in the same colours can still be two different products: one is a desk you read documents at, another a
console you watch all day, another a form a stranger fills in once. That difference is the ARRANGEMENT — and it is
where most of an app's character lives. The kit ships five, each built only from kit components, and each look in
`Osysharp.Ui.Looks` names the one it pairs with (`osy kit looks` prints it):

| look | arrangement | navigation | a record opens… | a change is kept… | acting on data | assistant |
|---|---|---|---|---|---|---|
| **Ledger** | the document desk | along the top (`TabbedShell`) | as tabs that go deeper — a list down a tab's side (`SubNav`), the chosen item as paper (`Paper`) | when a bar at the foot says Save (`SaveBar`) | a selection toolbar | — |
| **Relay** | the workbench | split: an activity bar + an explorer (`WorkbenchShell`) | docked on the right, with a live log below (`Dock`) | in place, in the drawer's head (`SaveActions`) | a selection toolbar | — |
| **Civic** | the flow | none — the steps are the map (`FocusedShell`) | one question group per page, a check of every answer at the end | "Save and continue", in words | — | — |
| **Studio** | the record desk | a labelled sidebar (`SidebarShell`) | beside the list, as tabs (`Dock`) | by itself, with an Undo while it waits (`AutoSave`) | a selection toolbar | over the page (`AssistantPanel`) |
| **Signal** | the command centre | an ink rail of marks (`RailShell`) | — the numbers are the page (`WidgetGrid`) | the person's own arrangement, kept inline (`SaveActions`) | — | over the page (`AssistantPanel`) |

**An arrangement is not bound to its look.** A finance team that wants a workbench takes `WorkbenchShell` and `Dock`
and keeps Ledger's paper and navy; a public service that needs a dashboard takes `WidgetGrid` in Civic. Pick the look
for how the app should FEEL, the arrangement for how it is USED.

## Description    {#description}

### Why these five?   {#why}

Business apps settle into a handful of shapes, and the five here are the ones that differ in how a person WORKS rather
than in decoration: reading records as documents (finance, legal, procurement); watching and acting at full density
(operations, support, developer tools); answering questions once, without training (public services, onboarding,
applications); working through people and records in passing (sales, recruiting, account management); and knowing
something at a glance (analytics, reporting). Each needs a different answer to the same five questions — where is
navigation, how does a record open, how is a change kept, where do I act on many things, where do I ask.

Two shapes that look like more archetypes are already covered as PAGES inside these: a board of cards by stage (the
specimen's pipeline page, in any arrangement) and conversation-first (an inbox with a thread and the person's context
beside it — `Dock` around a list of conversations, or the inbox kit). A "playful" or "calm" app is a LOOK, not an
arrangement: derive a look and keep the arrangement that fits the work.

### Which navigation style fits?   {#navigation}

| style | shell | for |
|---|---|---|
| top bar | `TabbedShell` | a handful of peer areas, the whole width left for the page; a bottom tab bar on a phone |
| sidebar | `SidebarShell` | many areas with a real hierarchy, always named |
| rail | `RailShell` | a few areas and a page that wants every pixel; marks that open to labels on demand |
| split | `WorkbenchShell` | sections, each with its own pages and views — the explorer lists the section you are in |
| none | `FocusedShell` | a task with a beginning and an end; the steps are the map and Exit asks before it drops anything |

All five take the same `AppChrome` and the same slots, so moving an app from one to another is one word ([App shells](https://osysharp.com/reference/ui/shell/)).

### How does a page show its unit of work?   {#saving}

`SaveBar`, `SaveActions` or `AutoSave` — three presentations of the same route unit of work, chosen by what a person
expects when they look away ([Save, discard, undo — three ways to show a page's unit of work](https://osysharp.com/reference/ui/saving/)).

### How is a document desk built? (Ledger)   {#document-desk}

```osy title="a record as tabs, one tab going deeper, the item as paper, kept with a bar" test app=ui-arrangement-document-desk
class Bill { public string Number; public string Total; }
List<Bill> Bills() { return [ new Bill { Number = "F-2041", Total = "1,840.00" }, new Bill { Number = "F-2007", Total = "960.00" } ]; }

entity Terms {
  int DaysToPay;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

[Layout] [AllowAnonymous]
component Desk() {
  render {
    TabbedShell(new AppChrome { Product = "Larkspur", Home = "/", Nav = [ new NavItem { Label = "Clients", To = "/", Icon = Icons.Building } ] }) {
      Outlet(retain: 8);
    }
  }
}

[Route("/")] [Layout(Desk)] [Title("Clients")] [Render(CSR)] [AllowAnonymous]
component ClientPage() {
  string open = "F-2041";
  live var terms = Terms.FirstOrDefault();
  action Show(string n) { open = n; }
  render {
    Stack(gap: 4) {
      PageHead("Wren & Pike");
      Tabs { Tab("Overview"); Tab("Invoices", selected: true); }
      Row(gap: 5, align: Align.Start) {
        SubNav("Client invoices") {
          foreach (var b in Bills()) { SubNavItem(b.Number, selected: open == b.Number, onPress: () => Show(b.Number)); }
        }
        Paper("Invoice " + open) { Text(Bills().First(b => b.Number == open).Total, fontFamily: Font.Figure); }
        if (terms != null) { NumberField("Days to pay", value: terms.DaysToPay, min: 7, max: 90); }
      }
      SaveBar(saved: "Terms saved.");
    }
  }
}
```

### How is a workbench built? (Relay)   {#workbench}

```osy title="sections and an explorer, a dense table, the opened row docked right, a log below" test app=ui-arrangement-workbench
class Alert { public string Id; public string Title; }
List<Alert> Alerts() { return [ new Alert { Id = "AL-31", Title = "Disk 91% on db-2" }, new Alert { Id = "AL-30", Title = "Queue lag 40s" } ]; }

[Layout] [AllowAnonymous]
component Bench() {
  render {
    WorkbenchShell(new AppChrome { Product = "Beacon", Home = "/", Nav = [
      new NavItem { Label = "Alerts", Icon = Icons.Bell, Children = [ new NavItem { Label = "Firing", To = "/", Icon = Icons.Bolt, Exact = true } ] },
    ] }) {
      Outlet(retain: 8);
      slot status { Lamp(lit: true, live: true, tone: Tone.Success); Text("Live"); }
    }
  }
}

[Route("/")] [Layout(Bench)] [Title("Firing")] [Render(CSR)] [AllowAnonymous]
component Firing() {
  string open = "";
  action Open(Alert a) { open = a.Id; }
  action Close() { open = ""; }
  render {
    Dock(right: open != "", rightTitle: "Alert " + open, onCloseRight: Close, bottom: true, bottomTitle: "Log") {
      DataGrid(label: "Alerts", rows: Alerts(), selectable: true, rowSelected: Open, columns: [
        new GridColumn<Alert> { Name = "Id", Label = "ID", Value = a => a.Id, Width = 90 },
        new GridColumn<Alert> { Name = "Title", Label = "Title", Value = a => a.Title, Fill = true, Title = true },
      ]) { slot selection { picked => Button("Acknowledge", onPress: () => Close(), size: Size.Sm); } }
      slot right { Text(open); }
      slot bottom { Text("10:24:07  db-2 disk 91%", fontFamily: Font.Mono); }
    }
  }
}
```

### How is a flow built? (Civic)   {#flow}

```osy title="steps as the map, one question group a page, saving said in words" test app=ui-arrangement-flow
enum Venue { [Label("A café")] Cafe, [Label("A gallery")] Gallery }

[Layout] [AllowAnonymous]
component Apply() {
  render {
    FocusedShell(new AppChrome { Product = "List a venue", Home = "/", Nav = [
      new NavItem { Label = "About the venue", To = "/", Exact = true },
      new NavItem { Label = "Check and send", To = "/check" },
    ] }) { Outlet(retain: 8); }
  }
}

[Route("/")] [Layout(Apply)] [Title("About the venue")] [Render(CSR)] [AllowAnonymous]
component AboutVenue() {
  Venue kind = Venue.Cafe;
  action Continue() { Navigation.Go("/check"); }
  render {
    Stack(gap: 5) {
      PageTitle("Tell us about the venue");
      RadioGroup("What kind of venue is it?", value: kind);
      Row { Button("Save and continue", onPress: Continue, tone: Tone.Primary, size: Size.Lg); }
    }
  }
}

[Route("/check")] [Layout(Apply)] [Title("Check and send")] [Render(CSR)] [AllowAnonymous]
component CheckVenue() { render { PageTitle("Check your answers before sending"); } }
```

### How is a record desk built? (Studio)   {#record-desk}

```osy title="a list with the record beside it, kept by itself, an assistant a press away" test app=ui-arrangement-record-desk
entity Contact {
  [Required, MaxLength(80)] string Name;
  [MaxLength(400)] string? Note;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

[Layout] [AllowAnonymous]
component Desk() {
  bool asking = false;
  string draft = "";
  action Toggle() { asking = !asking; }
  action Ask() { draft = ""; }
  render {
    SidebarShell(new AppChrome { Product = "Larkspur", Home = "/", Nav = [ new NavItem { Label = "Contacts", To = "/", Icon = Icons.Users } ] }) {
      Outlet(retain: 8);
      slot actions { Button("Ask", onPress: Toggle, tone: Tone.Ghost, size: Size.Sm); }
    }
    if (asking) { AssistantPanel("Assistant", [], value: draft, onSend: Ask, onClose: Toggle); }
  }
}

[Route("/")] [Layout(Desk)] [Title("Contacts")] [Render(CSR)] [AllowAnonymous]
component Contacts() {
  live var contacts = Contact.OrderBy(c => c.Name).ToList();
  Contact? open = null;
  action Open(Contact c) { open = c; }
  action Close() { open = null; }
  render {
    Dock(right: open != null, rightTitle: "Contact", onCloseRight: Close) {
      DataGrid(label: "Contacts", rows: contacts, rowSelected: Open, columns: [
        new GridColumn<Contact> { Name = "Name", Label = "Name", Value = c => c.Name, Fill = true, Title = true },
      ]);
      slot right {
        if (open != null) {
          Stack(gap: 3, p: 4) {
            Row(gap: 2, align: Align.Center) { Strong(open.Name); Spacer(); AutoSave(after: 2); }
            TextAreaField("Notes", value: open.Note, rows: 4);
          }
        }
      }
    }
  }
}
```

### How is a command centre built? (Signal)   {#command-centre}

```osy title="marks for navigation, the range above the numbers, cards a person arranges" test app=ui-arrangement-command-centre
enum Span { [Label("24 hours")] Day, [Label("7 days")] Week }

[Layout] [AllowAnonymous]
component Centre() {
  render {
    RailShell(new AppChrome { Product = "Larkspur", Home = "/", Nav = [ new NavItem { Label = "Live", To = "/", Icon = Icons.Bolt, Exact = true } ] }) {
      Outlet(retain: 8);
    }
  }
}

[Route("/")] [Layout(Centre)] [Title("Live")] [Render(CSR)] [AllowAnonymous]
component Live() {
  Span span = Span.Day;
  bool editing = false;
  action Pick(Span s) { span = s; }
  render {
    Stack(gap: 4) {
      Row(gap: 2, align: Align.Center) { PageTitle("Live"); Spacer(); Switch("Customise", value: editing); }
      Tabs { foreach (var s in Span.Members) { Tab(s.Label, selected: span == s, onPress: () => Pick(s)); } }
      WidgetGrid {
        Widget("Visits", editing: editing) { Metric("visits", "1,208"); }
        Widget("Orders", span: WidgetSpan.Two, editing: editing) { Metric("orders", "64"); }
      }
    }
  }
}
```

### Where are the full versions?   {#specimen}

Each archetype is built out in full — real rows, every pattern in its states, wide and on a phone, light and dark — in
the looks package's own test app, and photographed in its look with `osy test --pixels --theme Looks.X`. To see any
arrangement in any look, wear the look on your own pages: `osy compile --theme Looks.Relay`.

## See also       {#see-also}

- [Looks for business apps — start from a finished design and make it yours](https://osysharp.com/reference/ui/looks-kit/) — the five looks, and deriving your own
- [App shells](https://osysharp.com/reference/ui/shell/) — the `AppChrome` contract every navigation style shares
- [Dock](https://osysharp.com/reference/ui/dock/) · [Save, discard, undo — three ways to show a page's unit of work](https://osysharp.com/reference/ui/saving/) · [Acting on data — a calm grid header, filters as chips, and a toolbar that follows the selection](https://osysharp.com/reference/ui/selection/) · [SubNav](https://osysharp.com/reference/ui/subnav/) · [Paper](https://osysharp.com/reference/ui/paper/) · [WidgetGrid](https://osysharp.com/reference/ui/widget-grid/) · [AssistantPanel](https://osysharp.com/reference/ui/assistant-panel/)
- [WorkbenchShell](https://osysharp.com/reference/ui/shell-workbench/) · [FocusedShell](https://osysharp.com/reference/ui/shell-focused/) · [TabbedShell](https://osysharp.com/reference/ui/shell-tabbed/) · [RailShell](https://osysharp.com/reference/ui/shell-rail/)
