Summary#
Most applications with more than a handful of screens end up with the same frame: a side rail you navigate from, a work area the current screen renders into, and something pinned top and bottom. Building it is a day's work, and it is the same day in every app.
So the platform ships one — as an app you copy, not as controls you reference:
$ osy docs sample admin-shell --into ./my-app
$ cd my-app && osy launchYou get thirteen components in model/shell.osy that are yours to edit, a [Layout] that assembles them, and three
routed pages to prove the frame works. Everything they are built from — Button, Avatar, IconButton,
Spacer, the type roles, and the theme it all resolves against — stays Osyrin.Ui (the UI kit) and keeps improving without you.
Signature#
$ osy docs sample admin-shell --into ./my-app # the whole shell, as your sourceAppFrame { AppCard { SideRail { … } WorkArea { AppBar { … } Outlet(retain: 4); } } }Description#
Why this is a sample and not a kit control#
This used to be kit surface, and moving it out was a decision worth stating.
A shell is ONE navigation model, not a general one. These parts encode a collapsible left rail. Measured across the apps in this repository: an expense app is a top bar and used none of them; a file manager is a two-pane workspace and used none of them; the admin console is the rail — which is where they were extracted from. A library that serves the app it came from is a sample with extra ceremony.
A shell is where an app's identity lives. Shipping one as a library means every app that opts into the kit looks like the same product until it deliberately opts out — and opting out of thirteen components is not a small act.
And it claimed generic names. NavItem and Brand are words every app wants. While the kit held rail-specific
controls under them, an app's own top-bar pill collided with the library and had to justify itself in a comment.
The general rule this follows: genuinely reusable composables are the kit; the thing that makes your app yours is yours. A shell composes reusable controls — it is just not one itself.
What the sample gives you — the parts#
| Part | What it is |
|---|---|
AppFrame | The outer canvas — a padded backdrop on a tablet and up, full-bleed on a phone. |
AppCard | The floating card the app lives in. It clips its children, so a rail's edge and a table's scrollbar both stay inside the rounded corners. |
SideRail | The navigation column. Collapses to an icon strip. |
RailHeader / RailNav / RailFooter | Pinned top, scrolling middle, pinned bottom. Only the middle grows. |
RailSection | A labelled group of nav items; the label hides when collapsed. |
NavItem | One entry. current is the caller's to decide — a nav item has no business reading the URL. |
WorkArea | The column the routed page renders into. |
AppBar | A bar inside the work area — a hamburger row, a breadcrumb strip, a page toolbar. |
Brand | A mark in the slot, the product name beside it. |
WorkspaceTabs / WorkspaceTab | Open-document tabs, each with its own lifetime — a different model from Osyrin.Ui (the UI kit)'s Tabs, which switch a view within one page. An app wants one or the other, never both. |
There is no AppShell component — you assemble it#
Note what is not in that table: an AppShell. The moment a shell knows your routes, your navigation guard or your
nav list it stops being copyable, so the sample keeps the two apart — structure in model/shell.osy, policy in the
[Layout] below. That split is what lets you keep the frame and throw away every decision in it.
Three details that are load-bearing#
WorkArea sets MinW = 0
Without it, a wide table inside the work column shoves the entire shell sideways instead of scrolling within its own column — the single most common admin-layout bug, and invisible until real data arrives.
Why tabs keep their state — Outlet(retain: …)
retain is what makes tabs feel like tabs and makes going "back" cheap: a retained page keeps its state, so
switching away and returning does not throw away a half-filled form or re-run its reads. See routes and pages.
A WorkspaceTab's close button is a separate hit target
Closing never also selects. A close that selects first is the classic way to lose someone's work.
Examples#
The sample's own [Layout] is above — it IS the example, pulled from the app the docs build compiles. Scaffold it
and change one thing at a time:
$ osy docs sample admin-shell --into ./my-app
$ cd my-app && osy launchSee also#
- Osyrin.Ui (the UI kit) — the controls a shell is composed FROM, and
osy kitto read any of their source. - routes and pages —
[Page],[Layout],Outletandretain. - Slot (child content) — how the
{ … }block you pass each part reaches it. - layout primitives —
Stack/Rowand thegap/align/justifyvocabulary underneath all of this.