busy-office-ui
Menu

Pattern: app launch

Who uses it: anyone opening the suite, every session — the first screen after sign-in. What "done" looks like: the right app is one tap away, whether it's a favourite or three deep in a category. A suite entry screen — the icon-grid launcher (the "Bento UI" / tablet-launcher archetype from the device-coverage table — same .bo-widget-grid container-query mechanism as a dashboard, at a smaller tile size, not a new grid component). Tiles are plain links composed into .bo-widget cards with real CSS icons (themable currentColor glyphs) where a glyph is genuinely right, an initials chip where none is, and inline <svg> where a tenant brings their own; the category row is the ordinary tabs pattern; a "folder" tile is just a widget whose body holds a cluster of small tiles. Zero new CSS. Not for returning a user to a specific record they already know — a bookmark, a notification, or a saved link should land directly on the object page or the list report, never route back through the launcher grid to get there.

Favourites

All applications — category tabs

A "folder" tile — grouped apps

A folder is a single link to a category page (or a dialog listing its apps) whose face previews the members as badges — not a hover-expanding stack. One click, one navigation; the preview is aria-hidden so the link's accessible name is just "Finance apps".

A searchable launcher for a catalogue too big to scan

The grids above are the answer when someone can see their app. Past roughly two screens of tiles they cannot, and scrolling a category tree to find “Receivables” is slower than typing it. This is the same catalogue behind a dialog with a search box — composed from what already ships, with no new component and no new fullscreen modifier.

All applications

Finance

Logistics

Purchasing

Why this one is allowed to fill the viewport. A dialog is normally kept to a measure — 32rem, because prose stops being readable past it. A launcher is not prose: it is a grid the reader scans, and a narrow column turns one glance into a scroll. The sizing here is page-local, a few lines against this dialog’s own id, not a new --fullscreen modifier that every future dialog could reach for.

Which of the three to reach for. The tile grids above win when the catalogue fits on a screen or two — recognition beats recall, and no typing is faster than typing. This launcher wins when it does not fit and the reader knows roughly what the thing is called. The command bar wins when they know exactly, and want to jump without leaving the keyboard — it searches actions as well as apps, and it is a different keyboard model. Not for a list of records; that is a list report with a filter.

What is real here and what is not. Opening, the search, Escape, Close and the focus moves are real, and seven cases in check:claims drive them with real key and pointer events — including the one that used to be broken: with a query typed, type="search" carries a native Escape-to-clear that consumed the key, so closing took two presses. One press closes it now, and the build fails if that regresses. The tiles are href="#", like every other tile on this page — they go nowhere. The dialog needs JavaScript: initDialogs() opens it, so without JS the trigger does nothing and the catalogue above remains the way in. That is why the grids stay on this page rather than being replaced by the launcher. Filtering, the result count and what the tiles link to are yours.

Markup

<!-- A tile is a link styled as a card. The grid is the SAME .bo-widget-grid
     a dashboard uses — --bo-widget-min sets the tile size; the grid's own
     container query reflows it on tablet/narrow widths for free. -->
<div class="bo-widget-grid" style="--bo-widget-min: 9rem">
  <a href="/purchasing" class="bo-widget"
      style="text-align: center; padding-block: var(--bo-space-4)">
    <!-- No color on the mark: the reset's own a { color: accent } already
         gives the tile its accent, and the label below opts OUT of it. -->
    <span class="bo-icon bo-icon--cart" aria-hidden="true"
        style="font-size: 2rem; margin-inline: auto"></span>
    <span style="color: var(--bo-color-text-primary)">Purchasing</span>
  </a>
  …
</div>
<!-- Category row = the ordinary tabs pattern; each panel holds its own grid. -->

Anatomy

  1. App shell — header + sidebar + main; the shell is the pattern.
  2. Entry tiles — a .bo-grid of cards, each a task the user starts here.
  3. Counts / badges — "12 awaiting approval" on a tile: the reason this screen exists rather than a static menu.
  4. Recent items — a short list back into records the user touched last.

One glyph per app, never reused in a launcher. A tile grid is scanned by shape before it is read, so two apps wearing the same mark cost the user the whole benefit of the grid. If no distinct glyph exists, use the initials chip rather than an approximate one — an approximate mark is worse than letters, because it is confidently wrong. This page practises that: Payables and Receivables are AP and AR rather than two near-identical document outlines, which is exactly the bug this section now rules out.

The icon set is not the limit. A launcher is product content — your app list is yours, and no framework can ship the right glyphs for it. Any inline <svg> drops into the same slot and inherits currentColor, so it themes with everything else for free. Currencies above is one: the international currency sign, hand-drawn in six lines of SVG, with nothing added to the framework.

<a href="/currencies" class="bo-widget">
  <svg viewBox="0 0 24 24" width="32" height="32" fill="none"
       stroke="currentColor" stroke-width="2" aria-hidden="true">
    …your mark…
  </svg>
  <span>Currencies</span>
</a>

Data contract

RequestReturns / on failure
GET /homethe tile grid; counts may be computed asynchronously
GET /home/countsa fragment swapped into the badges once available — tiles must render WITHOUT it. On failure (5xx/timeout), the badges simply never appear; that is a silent degrade, not an error banner — a tile grid failing loudly over a missing count would be worse than the count just being absent
GET /recentthe recent-items list; empty for a new user

States

StateWhat ships
Loadingtiles render immediately — nothing here depends on data, only on which apps this user may open
Per-tile counts (if your deployment adds them)not part of AppTile's three marks (icon / initials / svg) — a count badge is a fourth kind this component does not ship; add it as a sibling element inside the tile, and drop it silently rather than showing a stale or zero count
Empty (new tenant)a first-run state naming the one setup action, not an empty grid
No permissionomit tiles the user cannot open — a disabled tile teaches nothing
Errorinline alert above the grid with retry; keep whatever tiles did load
No JSthe category tabs are plain <button role="tab"> elements with no native switching behavior — without initTabs()'s click handler, the second panel (marked hidden in the markup) never un-hides, so only the Finance tiles are ever reachable; the Logistics apps behind the second tab are effectively invisible

Components used

complexity 1 of 4

↑↓ move Enter open Esc close