busy-office-ui
Menu

Pattern: role home

Who uses it: every named role — a buyer, an approver, a warehouse supervisor — each seeing their own version. How often: the first screen after sign-in, and the one they come back to between tasks. What "done" looks like: the reader knows in one glance whether anything needs them, and leaves for the screen that resolves it. Not a launcher — a tile grid of every app someone could open is app launch; this shows live content about their work. And not a dashboard: the reporting dashboard monitors the business (is the period on track), a role home monitors my work (is anything waiting on me). Same widgets, different subject — and confusing the two is how a home screen turns into a chart wall nobody acts on.

The screen — one role's morning

Needs you Open inbox
Awaiting your approval 3 1 over 2 days, needs attention Exceptions assigned 2 oldest 26 days, needs attention Tasks 1
My open orders
Value in flight $184,320 across 14 POs
Period close

Period 2026-07 · 82% complete

2 of your 11 close tasks remain

Recent

Every number on this screen is about the reader — "awaiting your approval", "your 11 close tasks" — and every one of them is a link out. A role home that cannot be left is a wall; the measure of this screen is how fast people leave it for the screen that does the work.

One pattern, many roles

The same markup serves every role — only the card contents change, which is why this is one pattern and not one screen per job title. An approver's morning above; a warehouse supervisor's below:

Needs you Open inbox
Receipts to post 7 Short deliveries 2 needs decision, worse
Dock status
Trucks waiting 3 next slot 11:20

Identical classes, identical grid, different content. Roles differ in what the server sends, never in the markup — a per-role screen fork is the thing this pattern exists to prevent.

Markup

<div class="bo-widget-grid" style="--bo-widget-min: 16rem">
  <!-- The one card that must exist: what needs the reader, linking
       into the inbox that holds the rows. -->
  <section class="bo-widget bo-widget--span-2">
    <div class="bo-widget__header">
      <span class="bo-widget__title">Needs you</span>
      <a class="bo-btn bo-btn--sm bo-btn--secondary" href="/inbox">Open inbox</a>
    </div>
    <div class="bo-widget__body">
      <span class="bo-stat">
        <span class="bo-stat__label">Awaiting your approval</span>
        <span class="bo-stat__value">3</span>
        <span class="bo-stat__delta bo-stat__delta--bad">1 over 2 days</span>
      </span>
    </div>
  </section>

  <!-- Progress card: the close, a run, an onboarding checklist. -->
  <section class="bo-widget">
    <div class="bo-widget__header"><span class="bo-widget__title">Period close</span></div>
    <div class="bo-widget__body">
      <!-- Native <progress>: value/max semantics and the progressbar
           role come from the platform. Always pair with visible text. -->
      <progress class="bo-progress" value="82" max="100"
                aria-label="Period 2026-07 close progress"></progress>
    </div>
  </section>
</div>

Anatomy

  1. Identity line — a byline naming who is signed in and in what role. On a screen whose whole content is "yours", the reader must be able to see whose.
  2. "Needs you" card — the one card that must exist, spanning two columns, counting what the inbox holds and linking straight into it. This card is a summary OF the inbox, never a second copy of its logic. This is the human-monitoring signal — the AI-manages/human-monitors split below names it explicitly.
  3. Stat cards — stats with a label, value, and a delta that carries its meaning in words plus a glyph (two-channel: "1 over 2 days" is the text, the tone is the echo).
  4. Progress card — a progress bar for anything with a finish line: the period close, a posting run, onboarding.
  5. Recent — links back to what the reader touched; the cheapest useful card in the set.

Data contract

RequestReturns / on failure
GET /homethe grid shell with each card as its own fragment slot — the page must render before the slowest card resolves
GET /home/cards/:nameone card, loaded independently (hx-trigger="load"); one slow query must never hold the whole screen — the reporting dashboard's contract, same reason. On 5xx/timeout: that one card shows its own failure + retry (see States); the rest of the grid stays live
countsevery "needs you" number comes from the SAME query the inbox uses, not a parallel one — two sources for one number is how a home screen starts lying
per-role contentthe server decides which cards a role gets; the client never hides cards it received, because a hidden card is still a leaked fact

The AI-manages / human-monitors split, stated: this screen is almost entirely the human's side — it is the oversight surface for work agents are doing underneath. Its one machine-facing rule is the counts contract above: an agent that resolves an inbox row must move this screen's number with it, because a stale "3 awaiting" is worse than no number at all.

States

StateWhat ships
Nothing needs youthe "needs you" card says so as a success — "Nothing is waiting on you" — matching the inbox's own empty state; the card stays, it never disappears (a missing card reads as a broken card)
Card still loadinga skeleton inside that card only; the rest of the screen is already usable
Card failedthat card shows the failure and a retry, and the others carry on — one dead query must not blank the home screen
New role / first daycards render with real zeros and an explanation, never an empty grid: "No orders yet — your first PO will appear here"
Permission-limited rolethe server simply does not send those cards; nothing on screen hints at what was withheld
No JSfully functional — this page has no JS dependency; every card is static markup and there is no <script> block to lose

Components used

complexity 2 of 4

↑↓ move Enter open Esc close