App layout
aside-left
main
aside-right
work-area
Hover: the work area takes its place

Lay in. Lay out.

01 — The columns

Named by role, not by side

Four of them. A screen claims the ones it needs with <Layout.Pane column="…"> and leaves the rest unmounted.

aside-left
main
aside-right
work-area
aside-leftthe app

The product’s own navigation. Not part of any one screen — it is how you get between them.

mainthe conversation

Always the conversation. A hub before you have picked one, a chat once you have. The breadcrumb is the way back.

aside-rightthe context of what is in main

Belongs to whatever occupies main, and comes with it. Every row is a door into the work area.

work-areawhat you are working on

Mounted only when something is open. Opened by a row in the context column, by Odin from inside a chat, or by a chat that starts about a thing.

Chrome belongs in the pane’s card snippet, never in the body. A pane that draws its own frame stops matching the ones beside it the moment either changes.

02 — The rules

Three, and they are all about ownership

Who owns a column, who owns a width, and what a breakpoint is allowed to measure.

chat paneoverwritten
the hubowns it
main

A column holds one thing

Two components cannot claim the same column — registerPaneTrack overwrites the owner. So a route that takes main must leave CHAT_MAIN_ROUTES, and the shared chat pane then remounts whenever you cross between it and a route still on the list.

Card
Card
Card

The container owns the width

Card has no width prop. It fills its parent, and the width comes from the pane’s grid column or a dialog’s size tier. width: 480px on body content is a named anti-pattern: it puts a layout decision inside a component that cannot see the layout.

one window
1180aside shown
860aside hidden

A breakpoint measures the pane

One number, 1100px, and it is a container query on .layout-pane rather than a media query on the window. Two panes in the same window can sit on opposite sides of it.

Two of these are not tidy
720 is also size/180

The spacing page says layout widths are not spacing steps. Pane widths come off that scale anyway. One of the two documents has to give.

The 1100 is written twice

Once in layout.group-pane.svelte and once in layout.group-collapse-button.svelte, kept in step by a comment. Nothing enforces it.

03 — Two axes

main and work-area move independently

main changes when you change conversation. work-area opens and closes beside it while that conversation stays where it is.

So Odin can open something mid-chat without costing you your place, and closing the work area gives back the context column you had.

aside-left
main
aside-right
work-areanot mounted
a hub, nothing open
aside-left
main
aside-right
work-areanot mounted
a chat, its context beside it
aside-left
main
aside-right
work-area
and something open in the work area
What this rules out

Closing the work area is not a way back to the hub. If you were in a chat you are still in that chat, because main never moved.

04 — The hub

One pattern built on those columns

A hub is how you start or return to the conversations about the thing you have open. Opening a plan, a design kit or a dashboard lands you on its hub rather than inside one of its tools.

Before hubs, opening a design kit landed on a contents table and opening a plan landed on a calendar. That picks a tool for you, hides the rest, and leaves the conversations about the thing nowhere you can find them again.

The entity’s root route is the hub. Whatever used to live there moves down a level and keeps working — a design-kit item opened from a chat still needs somewhere to land.

What the hub column holds, top to bottom
  1. Breadcrumbthe way back out, and the context-column toggle
  2. AskOdinEntrythe composer, anchored to the entity’s URN
  3. Process barwhat this kind of entity would do for you
  4. Agendawhat is waiting on you here
  5. ChatTablethe conversations
05 — Two contexts

There are two aside-rights, not one that swaps

Each occupant of main brings its own context column. They are different columns with different rules, and reading them as one column with changing contents gets the behaviour wrong.

01

When the hub is in main

aside-left
mainthe huba plan, a kit, a dashboard
aside-right
Brief
Guidelines
Contents

The same three rows every time you open that kind of thing. Written into the entity type, not gathered.

02

When a chat is in main

aside-left
maina chatone conversation
aside-right
The Q3 planmentioned
Audience: returningreferenced
Last week’s dashboardopened
and whatever comes next

Whatever this conversation has touched. Different in every chat, and longer at the end of one than at the start.

Every row in either one is a door into the work area.

aside-right and work-area can be shown together — the layout allows it. The hubs choose to swap them. That is a product decision and should not be repeated as though the layout forced it.

06 — Anchoring

A hub needs something to be about

A hub anchors its composer to a URN and filters its table on the same one. Entities have one: a plan is urn:focause:plan:{id}, and everything follows.

Sports, Content and responsible-gaming-as-an-area do not. They are regions of the product rather than things, and nothing in the identity scheme mints an aggregate for them. Two workarounds have already shipped and they answer the question differently.

Dashboards

The section lists its children’s conversations — selectChatRowsForContexts(chats, reportUrns). The section anchors nothing itself, and the table reads as “conversations about your dashboards”.

Content

An invented frontend-only URN, urn('area', 'content'). Nothing on the backend mints or reads it. A chat created with it comes back in this hub’s table and nowhere else.

And some things should not have one

You arrive at a contact because something pointed you there, you look, and you leave. Nobody holds a conversation about a person, and a nav entry opening a searchable list of every contact invites browsing a customer database — not a job Odin is for.

A hub is for things people talk about. That test is worth applying before the URN question is even asked.

Neither is settled

#4238 asks whether an area can carry a chat context at all, and it may be answered with a no. Minting urn:focause:area:sports would put a navigation grouping into an identity scheme that is load-bearing across the backend, the agent and the warehouse.

07 — Moving between routes

One knob, four phases

A route change is choreographed rather than instant. Everything derives from --layout-total-duration, which is 700ms multiplied by the product-wide motion scale — the same setting the motion page documents. Turn motion down and the layout slows with everything else; turn it off and it stops.

content flies out
191ms
the page you are leaving lifts 5px and fades
pane delay
255ms
nothing moves yet — the old content is still going
panes move
255ms
frame slide, fade, and the grid resizes
content flies in
191ms
the new page settles the same 5px

The fractions are constrained, not free: the old content must be gone before the panes start, and the four have to add to one. Content travels 5px, which is enough to read as movement and not far enough to look like a slide.

The variables sit on :root because they have to reach the ::view-transition-* pseudo-elements, which are anchored there rather than inside the grid.

08 — Traps

Things that have already cost a day

One occupant per column

registerPaneTrack overwrites the column owner. Two things cannot claim main, which is the whole of #4219.

resolve() returns a relative path

It gives you ../design/x, which is what an href wants and is useless against page.url.pathname. Comparing them silently leaves the work area mounted and suppresses the context column. Compare against a literal path.

<Card fill> hides its overflow

The body is overflow: hidden, so a hub column needs a ScrollArea or everything below the fold is cut off with no scrollbar to tell you.

The agenda is empty unless it is a plan

Proposals carry a planId, so nothing can be proposed about a design kit yet. Render the section anyway — it is where entity proposals will land.

The process bar does not click

No card carries a count and nothing in it is a target. There is no source for those numbers, and one invented to fill the space would have the surface arguing from evidence that does not exist. A step gains a click and a count together.

09 — Not decided

What this page cannot tell you yet

These are open in the tree, not open in the writing. The page will say so until they close.

Two have closed already. CMS, localization and files did merge, and it is live. And #4253 — write the real layout-patterns document — is this page.

#4219

A route cannot take over main and keep the shell’s shared chat pane. Leaving CHAT_MAIN_ROUTES remounts the chat — thread, SSE session and scroll all reset. The cost grows with every hub, because the shared pane is only shared across the routes still on the list.

Johan, Tobias
#4238

Can an area carry a chat context at all? Sports, Content and RG-as-an-area are regions of the product, not things, and nothing mints a URN for them. It may be answered with a no.

Adam
#4234

Is the responsible-gaming hub per alert or per area.

And this page’s own gap

The layout package’s SPECS.md has not been read into this document. Column widths, what aside-left holds, how many panes a screen may have, and the z/ ladder between below and toolpanel are all still described from the outside.