# Pinned and recent pages, the app switcher, and presence in a shell

> A work shell draws the reader's own shortcuts — the pages they PINNED and the ones they opened RECENTLY — above its nav, turns the brand into an app switcher when the app lists other applications, puts a pin beside the page title, and shows the signed-in person's presence. The shell keeps no list of its own: pins and visits are rows the app keeps per user and hands in.

<!-- id: ui-shell-shortcuts · area: ui · stability: preview · html: https://osysharp.com/reference/ui/shell-shortcuts/ -->

## Summary        {#summary}
An all-day tool is used the same few ways every day, so its shell carries the reader's own shortcuts: the pages they
pinned and the pages they opened last. A shell built on `AppChrome` draws both as sections above its nav (Sidebar,
Rail) or in the switcher behind its brand (Tabbed), draws a pin beside the page title that pins the page you are on,
lists the other applications the reader can open behind the brand, and lights a presence lamp on their avatar. The
kit only DRAWS: which pages are pinned is a fact about a person, so the app keeps it as rows and hands it in.

## Signature      {#signature}
```osy syntax
new AppChrome {
  Nav = …,
  Pinned = pins.Select(p => new NavItem { Label = p.Label, To = p.Route, Icon = p.Icon }).ToArray(),
  Recent = visits.Select(v => new NavItem { Label = v.Label, To = v.Route, Icon = v.Icon }).ToArray(),
  TogglePin = TogglePin,                       // an action: pin or unpin Navigation.CurrentPath
  Apps = [ new AppLink { Label = "…", To = "/", Current = true }, new AppLink { Label = "…", To = "https://…" } ],
  User = new ShellUser { Name = …, Presence = ShellPresence.Online },
}
```

## Description    {#description}

### The state is the app's, per person   {#state}
`Pinned` and `Recent` are plain `NavItem[]` the shell is handed. The kit keeps no list and no browser storage, on
purpose: a pin made on a laptop has to be there on the phone, so it is a ROW with an owner, and the layout reads the
signed-in person's rows live — a pin added on this page appears in the rail at once. `AppChrome.ItemFor(route)` names
a route from the nav (its label and icon), which is what a new pin or visit should be called; it answers null for a
route the nav does not cover (a record page), where the app names the row itself.

**Recording a visit** is an `on change` in the layout that reads only `Navigation.CurrentPath` and calls a server
function. Do not read the live chrome there: it reads the visits, so recording one would wake the block again — name
the route from the nav alone (below). **Recent is drawn without the page you are on** (it is already the lit nav row)
and without pinned pages, so a page is never listed twice; five or six rows are plenty.

### Where each shell draws them   {#where}
| shell | pinned and recent | app switcher | pin button |
|---|---|---|---|
| `SidebarShell` | sections above the nav; glyphs between hairlines when the rail is an icon strip | the brand | beside the page title |
| `RailShell` | glyphs above the nav in the strip, labelled in the drawer and the expanded panel | the brand at the rail's head | beside the page title |
| `TabbedShell` | in the switcher behind the brand, under the apps — a bar has no rail for them | the brand | beside the page's actions |
| `FocusedShell` | not drawn — a focused task is not a place to wander off from | the brand stays a link home | not drawn |

A pinned row for the page you are on reads as *trail* (weight and ink, no fill), so the nav's own row stays the one
lit row on the screen.

### The app switcher   {#switcher}
With `Apps` empty (the default) the brand is a plain link home. With one or more, the brand opens a connected menu:
the product and its tagline, **Home**, then **Apps** with the one marked `Current` ticked. An `AppLink.To` is a route
in this app or another app's full address, which opens in the same tab as switching apps does everywhere.

### The pin   {#pin}
Set `TogglePin` and the shell draws a pin beside the page title — a toggle button (`pressed:` when the page is in
`Pinned`), named for what a press will do ("Pin this page" / "Unpin this page"), whose glyph nudges when the state
flips. The action reads `Navigation.CurrentPath`, so it pins whatever page is open, record pages included.

### Presence   {#presence}
`ShellUser.Presence` — `None` (the default, draws nothing), `Online`, `Away`, `Busy` (labelled "Do not disturb") —
puts a lamp on the avatar's corner (breathing when online) and the word under the name in the account menu, where a
screen reader hears it. Map an app's own presence states onto these four.

## Examples       {#examples}
Pins and visits owned by the signed-in person, an app switcher, and presence — the whole floor in one layout:

```osy title="per-person pins and recent pages, an app switcher and presence on a SidebarShell" test app=ui-shell-shortcuts
using Osysharp.Ui;

[Principal] entity User {
  [Unique, MaxLength(200)] string Email;
  [MaxLength(120)] string Name;
  security { allow read when IsAuthenticated; }
}

// A pin belongs to a PERSON — it follows them to every device they sign in on.
entity Pin {
  User Owner;
  [MaxLength(300)] string Route;
  [MaxLength(120)] string Label;
  Icons Icon;
  DateTime PinnedAt;
  security { allow read, create, delete where Owner == user; }
}

entity Visit {
  User Owner;
  [MaxLength(300)] string Route;
  [MaxLength(120)] string Label;
  Icons Icon;
  DateTime At;
  security { allow read, create, update where Owner == user; }
}

void Remember(string route, string label, Icons icon) {
  var me = Session.CurrentUser;
  var seen = Visit.FirstOrDefault(v => v.Owner == me && v.Route == route);
  if (seen != null) { seen.At = DateTime.UtcNow; }
  else { new Visit { Owner = me, Route = route, Label = label, Icon = icon, At = DateTime.UtcNow }; }
}

void TogglePinFor(string route, string label, Icons icon) {
  var me = Session.CurrentUser;
  var pin = Pin.FirstOrDefault(p => p.Owner == me && p.Route == route);
  if (pin != null) { pin.Delete(); }
  else { new Pin { Owner = me, Route = route, Label = label, Icon = icon, PinnedAt = DateTime.UtcNow }; }
}

[Layout]
component Shell() {
  var me = Session.CurrentUser;
  live var pins = Pin.Where(p => p.Owner == me).OrderBy(p => p.PinnedAt).ToList();
  live var visits = Visit.Where(v => v.Owner == me).OrderByDescending(v => v.At).Take(6).ToList();

  NavItem[] Map() {
    return [
      new NavItem { Label = "Inbox", To = "/", Icon = Icons.Home },
      new NavItem { Label = "Reports", To = "/reports", Icon = Icons.Chart },
    ];
  }
  // Named from the NAV, never from the live chrome — which reads the visits the block below records.
  NavItem Named(string route) { return new AppChrome { Nav = Map() }.ItemFor(route); }

  on change {
    var path = Navigation.CurrentPath ?? "";
    var item = Named(path);
    if (item != null) { Remember(path, item.Label, item.Icon); }
  }
  action TogglePin() {
    var path = Navigation.CurrentPath ?? "";
    var item = Named(path);
    if (item != null) { TogglePinFor(path, item.Label, item.Icon); }
  }

  render {
    SidebarShell(new AppChrome {
      Product = "Harbourmaster",
      Nav = Map(),
      Pinned = pins.Select(p => new NavItem { Label = p.Label, To = p.Route, Icon = p.Icon }).ToArray(),
      Recent = visits.Select(v => new NavItem { Label = v.Label, To = v.Route, Icon = v.Icon }).ToArray(),
      TogglePin = TogglePin,
      Apps = [
        new AppLink { Label = "Harbourmaster", To = "/", Current = true },
        new AppLink { Label = "Tide tables", To = "https://tides.example.com/", Icon = Icons.Chart },
      ],
      User = new ShellUser { Name = me.Name, Initials = "HM", Presence = ShellPresence.Online },
    }) { Outlet(retain: 8); }
  }
}

[Route("/")] [Layout(Shell)] [Title("Inbox")]
component Inbox() { render { Text("Berth requests waiting"); } }

[Route("/reports")] [Layout(Shell)] [Title("Reports")]
component Reports() { render { Text("Quay occupancy"); } }
```

### Testing them   {#testing}
The pin is pressed by its name and the rail's sections are scoped by theirs:
`Ui.Click("Pin this page"); Assert.Visible("Reports", within: "Pinned");` · `Assert.Visible("Inbox", within: "Recent")`.
The switcher opens from the brand: `Ui.Click("Harbourmaster"); Assert.Visible("Tide tables", within: "Switch app")`.
`demo/shell-showcase` and `demo/shell-arrangements` carry these tests for every work shell.

## See also   {#see-also}
- [App shells](https://osysharp.com/reference/ui/shell/) — the `AppChrome` record and the shells that draw it
- [on change](https://osysharp.com/reference/ui/on-change/) — the reaction that records a visit
- [Menus, the command menu, and keyboard shortcuts](https://osysharp.com/reference/ui/menus/) — the connected menu the switcher is
- [animationKey — play an animation again when a value changes; a line that draws itself](https://osysharp.com/reference/ui/animation-key/) — the nudge a count or the pin gives when it changes
