# FocusedShell

> An app shell for completing a single task — a checkout, an approval, a configuration step, a guided flow. It is built around three things a task needs: a clear primary action, your position in a sequence, and a way out. It reads the app's own Nav as an ordered flow, so switching to it declares nothing new.

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

## Summary        {#summary}

`FocusedShell` is the arrangement for **completing one thing**: a checkout, an approval, a data-entry or
configuration step, a guided multi-step flow.

⛔ **It is task-oriented, which is not the same as minimal.** A later `Immersive` shell is the experience-oriented
one — a map, a photo editor, a drawing tool, where the content *is* the point. Build this shell by subtraction and
the two become one shell wearing two names. What defines it is what a task needs: **a clear primary action**, a
**position in a sequence**, and a **way out**. The sparse navigation is a *consequence* of those, not the design.

```osy title="a checkout, which is the shape this arrangement is for" test app=ui-shell-focused
[Layout]
[AllowAnonymous]
component FocusedLayout() {
  action SaveDraft() { }

  render {
    FocusedShell(new AppChrome {
      Product = "Expensely",
      Tagline = "New expense claim",
      Home = "/",                       // the way out
      User = new ShellUser { Name = "Olivia Rhye", Initials = "OR" },
      Nav = [
        new NavItem { Label = "Details", To = "/f", Icon = Icons.File, Exact = true },
        new NavItem { Label = "Receipts", To = "/f/receipts", Icon = Icons.Plus, Children = [
          new NavItem { Label = "Upload", To = "/f/receipts/upload", Icon = Icons.Plus },
        ] },
        new NavItem { Label = "Review", To = "/f/review", Icon = Icons.CheckCircle },
      ],
    }) {
      Outlet(retain: 8);
      slot actions { Button("Save draft", onPress: SaveDraft, tone: Tone.Ghost, size: Size.Sm); }
      slot aside {
        ShellAside("Claim summary") {
          Text("Total", fontWeight: FontWeight.Semibold);
          Text("EUR 1,240.00", fontSize: FontSize.Heading);
        }
      }
    }
  }
}

[Page("/")] [Layout(FocusedLayout)] [Title("Home")] [Render(CSR)] [AllowAnonymous]
component FHome() { render { PageHead("Home"); } }

[Page("/f")] [Layout(FocusedLayout)] [Title("Details")] [Render(CSR)] [AllowAnonymous]
component FDetails() { render { PageHead("Claim details"); Card("Basics") { Text("A form."); } } }

[Page("/f/receipts")] [Layout(FocusedLayout)] [Title("Receipts")] [Render(CSR)] [AllowAnonymous]
component FReceipts() { render { PageHead("Receipts"); } }

[Page("/f/receipts/upload")] [Layout(FocusedLayout)] [Title("Upload")] [Render(CSR)] [AllowAnonymous]
component FUpload() { render { PageHead("Upload a receipt"); } }

[Page("/f/review")] [Layout(FocusedLayout)] [Title("Review")] [Render(CSR)] [AllowAnonymous]
component FReview() { render { PageHead("Review and submit"); } }
```

## Signature      {#signature}

```osy syntax
FocusedShell(AppChrome chrome) {
  Outlet(retain: 8);          // the routed page — a CENTRED column with a readable maximum
  slot search   { … }         // the bar, on a pointer. A reader wants this; a checkout will not fill it
  slot actions  { … }         // the bar — "Save draft" is the wizard-shaped one
  slot railFoot { … }         // no rail: under the page, inside the same centred column
  slot aside    { … }         // the SUMMARY column — beside the page at 1100+, under it below that
  slot primary  { … }         // THE COMMIT BAR — `ShellTaskBar`. In the task's own column, under the form
}

class FlowStep {              // what `AppChrome.Steps(current)` projects `Primary()` into
  NavItem Item;  int Number;  bool Done;  bool Active;
}
```

## Description    {#description}

### How does the reader finish?   {#primary}

`slot primary`, and it is the feature that makes this shell task-shaped. Fill it with `ShellTaskBar`, which pins a
commit bar to the foot of the task **inside the page's own column**, so the button that finishes the form sits
directly under the form.

```osy title="the commit bar a task ends with" syntax
slot primary {
  ShellTaskBar {
    Button("Back", onPress: Back, tone: Tone.Ghost);
    Button("Continue", onPress: Next, tone: Tone.Primary);   // last, and the only Primary one
  }
}
```

⚠ **A task you cannot see how to finish is not focused, it is undecorated.** This is the line between this shell
and the experience-oriented one, so if you fork it, keep it.

⚑ **The slot exists on all four arrangements**, so switching never *drops* an app's primary action — the record's
promise is that you lose the layout and nothing else. It is only this shell that is built around it.

### Where does this shell actually go? A ROUTE, not an application   {#where}

⛔ **No application is a four-step wizard.** Reach for `FocusedShell` for **one route group inside a larger app** —
create-claim inside an expense tool, checkout inside a storefront, onboarding inside a product. You enter the task,
you finish it, and you come back to the application you came from.

That needs nothing built, because **`[Layout(…)]` is a per-page attribute**: the task's pages name the focused
layout, every other page names the app's own, and moving between them is an ordinary navigation.

```osy title="one app, two shells, one press apart" syntax
[Page("/")]            [Layout(AppLayout)]   component Home() { … }   // TabbedShell — the application
[Page("/claim")]       [Layout(ClaimLayout)] component Details() { … } // FocusedShell — the task
[Page("/claim/review")][Layout(ClaimLayout)] component Review() { … }
```

Set `chrome.Home` on the task's layout to the **application's** home, so the exit leaves the task and lands
somewhere real. A flow whose exit points at its own first step exits nothing.

⚑ **Some applications genuinely ARE one guided task** — a checkout, a tax filing, a permit application. That reading
is equally correct; there `chrome.Home` is the flow's own start, which is the honest answer when there is nowhere
else to go back to. `demo/shell-arrangements` ships both: the claim at `/t/claim/*` is the task-inside-an-app
reading, and the vehicle registration at `/f/*` is the standalone one.

### Where did the navigation go?   {#no-nav}

There is none, and that follows from the model rather than standing on its own: a route out of the task is an
invitation to abandon it. This is the one arrangement that puts **no** `nav` landmark on the page, which is worth
asserting in your own tests if you fork it — the way this shell decays is somebody "improving" it with a nav.

It still gets the two things a wandering shell cannot give a task:

- **Position in a sequence.** The app's own `Nav` is re-read as a linear flow (see below), so switching arrangement
  declares nothing new.
- **A way out**, always in the same place and always a real link to `chrome.Home`. A modal task with no visible exit
  is the most complained-about pattern in checkout design.

And **room**: the page is a centred column with a readable maximum rather than full-bleed. This is the one place in
the kit where centring is right, because there is exactly one thing to read.

⚠ **The exit does not yet protect unsaved work.** `chrome.Home` is a route, so leaving is a plain link and a
half-finished task is simply abandoned. Making it confirm needs something the contract cannot express today — a
shell cannot ask whether a named slot was filled, so a `slot exit` with a shell-drawn fallback is impossible, and an
`AppChrome.OnExit` would put a field on the shared record that three of four arrangements have no use for. Until
that call is made, a "Save draft" in `slot actions` is the pattern that works.

### How does the nav become steps?   {#steps}

`AppChrome.Steps(current)` numbers `Primary()` — the top level, with any `Section` replaced by its children — and
marks the entry holding the current route as active, everything before it as done. A top-level entry is a **step**;
its `Children` are that step's **parts**, nested under it while it is the active step, never as steps of their own.

⛔ **The stepper is a COLUMN beside the task, not a strip above it**, and that is a taxonomy decision rather than a
layout preference. Horizontal, it had the same silhouette as [TabbedShell](https://osysharp.com/reference/ui/shell-tabbed/)'s tab strip — a top bar, then a
full-width row of labelled items, then the page — and those two shells can sit **one press apart in the same
application**. Their models are opposites: tabs are parallel destinations you may choose in any order and which
persist for the life of the app; steps are sequential positions inside one task, which you cannot jump ahead in and
which vanish when the task ends. A row that looks like tabs invites the reader to click ahead to step four.

Down the side, it is unmistakably a sequence at a glance; it uses the room a bounded task column was otherwise
wasting; and it composes into the classic three-column checkout — **steps · task · summary**.

⚠ **Assert it geometrically or not at all.** `Assert.Visible("Receipts", within: "Progress")` is true of a step
wherever it sits, which is why a full suite stayed green while the silhouette was wrong. `Assert.Below` and
`Assert.LeftOf` are the only assertions that can see a shape.

⚠ **Scope to `"Current task"`, not to `"Page"`.** The stepper sits inside the page region, so `Page` means
*[steps | task]* and a geometric claim against it cannot tell the two columns apart.

⚠ **`Done` is order, not history.** The shell knows the flow's shape and never which steps the reader actually
completed — a step reads as done because it comes *before* the one holding the current route. An app that tracks
real completion should reorder or trim `Nav`; nothing in the shell can know it.

⚠ **A route the flow does not cover shows no position at all** — no counter, no stepper. That is right rather than a
gap: a confirmation page is genuinely outside the numbered part, and inventing a position for it would be a lie.

### What changes at each width?   {#bands}

| band | the bar | progress |
|---|---|---|
| compact `< 768` | mark · "Step 2 of 4" and the step's name · actions · identity · exit | a 4px rule |
| cozy `768+` | mark · actions · identity · exit | a **stepper column** left of the task: every step, ticked when done, with the active step's parts nested under it |
| wide `1100+` | the same | the same, and the `aside` joins as a third column — steps · task · summary |

⚑ **Mobile loses the stepper and nothing else.** Four labelled steps do not fit down the side of a 390px screen —
there is no side — so the phone gets the count and the name of the step it is on, which is the part that answers
"where am I". Every step is still counted, the exit is still there, and the summary still arrives under the page.

⛔ **Every piece of chrome must earn its place against "the task matters more than the application".** Three were
cut on this argument:

| cut | why |
|---|---|
| the brand **tagline** (`ShellBrand(tagline: false)`) | a workspace subtitle is a fact about the *application*; the stepper already names the task |
| the **progress rule** at pointer widths | a second answer to the question the stepper already answers richly. The phone keeps it, because there it is the only answer |
| the horizontal **stepper** | see above |

**Identity stays, and that is a decision rather than an oversight.** It is the only route to sign out and to the
appearance control, and a shell whose whole design is to trap the reader in a task is exactly the one where being
unable to reach your own account is worst. It is mark-only, so it costs 28px. The `search` slot is still *placed* —
most task apps will not fill it and an unfilled slot draws nothing, but a reader (a long document, a policy you must
accept) genuinely wants search within the task, and silently discarding a slot an app filled would lose content.

### Where the summary goes   {#aside}

`aside` matters more here than in any other arrangement: a checkout's order total or a wizard's context is the
canonical case, and it is why the page and its aside sit side by side from 1100px up. Below that it stacks under
the page rather than disappearing, because a phone that silently loses a panel has lost content, not layout.

## Examples       {#examples}

```osy title="asserting the omission, which is the feature" syntax
// Every other arrangement puts a `nav` landmark named "Main navigation" on the page. This one must not.
Assert.Hidden("Main navigation");

// The active step's PARTS are on screen; the parts of a step you are not on are not.
Ui.Visit("/f/receipts");
Assert.Visible("Upload", within: "Receipts parts");

// The page is a bounded column rather than full-bleed — invisible to any text assertion.
Assert.Narrower("Page", "Page header");
```

## Notes          {#notes}

⚠ **The stepper is a `region` named "Progress", not a second `nav`.** These are not places to go; they are where you
are in one task, and calling them navigation would put a navigation landmark in a shell that deliberately has none.

## See also       {#see-also}

- [App shells](https://osysharp.com/reference/ui/shell/) — the shared `AppChrome` contract every arrangement reads
- [TabbedShell](https://osysharp.com/reference/ui/shell-tabbed/) — top tabs, and a bottom bar on a phone
- [RailShell](https://osysharp.com/reference/ui/shell-rail/) — a permanent icon rail, when the canvas is the product
- [routes and pages](https://osysharp.com/reference/ui/routing/) — `[Page]`, `[Layout]`, `Outlet` and retention
