Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

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.

stable5 examples compiled by CIuikitlayoutlooks

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):

lookarrangementnavigationa record opens…a change is kept…acting on dataassistant
Ledgerthe document deskalong 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—
Relaythe workbenchsplit: 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—
Civicthe flownone — the steps are the map (FocusedShell)one question group per page, a check of every answer at the end"Save and continue", in words——
Studiothe record deska labelled sidebar (SidebarShell)beside the list, as tabs (Dock)by itself, with an Undo while it waits (AutoSave)a selection toolbarover the page (AssistantPanel)
Signalthe command centrean 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#

Why these five?#

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.

styleshellfor
top barTabbedShella handful of peer areas, the whole width left for the page; a bottom tab bar on a phone
sidebarSidebarShellmany areas with a real hierarchy, always named
railRailShella few areas and a page that wants every pixel; marks that open to labels on demand
splitWorkbenchShellsections, each with its own pages and views — the explorer lists the section you are in
noneFocusedShella 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).

How does a page show its unit of work?#

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).

How is a document desk built? (Ledger)#

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)#

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)#

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)#

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)#

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?#

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#

Related

Looks for business apps — start from a finished design and make it yours

Osysharp.Ui.Looks ships seven finished looks for any app built on the UI kit — Ledger, Relay, Civic, Studio, Signal…

App shells

An app shell is the frame around every page: a brand, a navigation tree, the signed-in person and a menu behind them, a…

WorkbenchShell

The arrangement of a developer tool or an operator's console: an activity bar with one mark per section, an explorer…

Dock

The page's drawers: the page in the middle, a drawer docked on the right (the record a row opened, an inspector) and…

Save, discard, undo — three ways to show a page's unit of work

Every edit on a page joins the route's unit of work; how the page OFFERS to keep it is a choice of presentation…

Acting on data — a calm grid header, filters as chips, and a toolbar that follows the selection

A DataGrid reads calmly: its per-column filter row is shut until somebody asks to filter (the Filter toggle, or a…

SubNav

Navigation inside a tab: a list down the side of a tab's body that moves between that tab's own places — a record's…

Paper

A sheet of paper on the page's ground — a reading width, generous margins, the theme's hairline and lift — so an…

WidgetGrid

A workspace of cards — figures, charts and lists side by side on a four-column grid (two on a tablet, one on a phone)…

AssistantPanel

An in-app assistant over the page: a panel floating at the right edge (a full-screen sheet on a phone) with the…

FocusedShell

An app shell for completing a single task — a checkout, an approval, a configuration step, a guided flow. It is built…

TabbedShell

An app shell whose primary navigation is a horizontal strip of tabs under the brand row, and a bottom tab bar within…

RailShell

An app shell built around a permanently narrow icon rail, in the shape Slack, Linear and Discord converge on. Every row…