<!-- https://getkonjo.com/design/studio/dashboard · source: docs/design/studio/dashboard.md -->

# Dashboards and snapshot cards

Studio's landing shape. A grid of cards that answer one question — *what needs me today?* —
and hand off to the section that can do something about it.

> Part of the [Konjo design language](../konjo-design-language.md). The laws in the spine
> apply here unless this chapter contradicts them. Studio-specific — see
> [the dialect](dialect.md) and [Studio metrics](metrics.md).

This chapter exists because a completeness test specified Studio's Home screen and found the
whole shape described in twenty words — *"snapshot cards with a title, a few key numbers/rows,
and a '→' link to the full section"* — next to a table chapter of 158 lines and a form chapter
of 323. The agent invented a grid, a gutter, a span rule, a panel radius, a card-row anatomy,
an internal spacing scale, a card-height rule, an ordering rule and a state model, and every
one of those inventions was a place two engineers would have chosen differently.

## When a dashboard

- The person is **triaging**, not browsing — they came to find out what changed, not to look
  something up.
- The answer is **spread across unrelated domains** (money, attendance, testing, people) and
  no single list holds it.
- Every region has a **real home elsewhere**, and this screen is the doorway, not the desk.

If one domain dominates, that is a [table](tables.md) or an index with filters, not a
dashboard. A dashboard whose cards are all attendance is the attendance screen, wearing six
boxes.

## Anatomy

```
HOME                             eyebrow, text.secondary
Blue Heron Martial Arts          display 34/800 — the DOJO, not the screen name
Saturday, Aug 23                 body 15/400, text.secondary
( Blue Heron ) ( Riverside )     dojo chips, only when there is more than one
                            ↕24
┌── the alert slot — at most one, and usually empty ──────────────────────┐
│ Adult Intermediate at 7:15pm has no instructor.       Assign a sub →    │
└─────────────────────────────────────────────────────────────────────────┘
                            ↕24
┌ Today ─────────────── span 2 ─┐ ┌ Needs attention ─────┐
│ Today                4 classes│ │ Needs attention     7 │  title 22/700 ⟷ caption
│                          ↕12  │ │                       │
│ Kids Beginner                 │ │ Ana Torres            │  rowTitle 16/600
│ 6:00pm · Marcus Webb          │ │ ( Waiver missing )    │  caption / pills
│ · 8 booked                    │ │                       │
│                          ↕12  │ │                       │
│ Adult Intermediate  ( No sub )│ │ …                     │
│ 7:15pm · Unassigned           │ │                       │
│ · 12 booked                   │ │                       │
│                          ↕16  │ │                  ↕16  │
│ Open the schedule →           │ │ Open the risk queue → │  label 13/700, pinned to the floor
└───────────────────────────────┘ └───────────────────────┘
                            ↕24
┌ Growth ──────┐ ┌ Testing ─────┐ ┌ Birthdays ───┐
└──────────────┘ └──────────────┘ └──────────────┘
                            ↕24
┌ Money ─────────────── span 2 ─┐
└───────────────────────────────┘
```

The [screen inventory](ia-and-inventory.md) puts **six** cards on Home — Money, Today, Needs
attention, Growth, Testing, Birthdays — and Money is the one blocked on a
[product fact](../product-facts.md) (refund and revoke semantics), not on a design one. The
sketch shows six so nobody builds five and calls it done.

## The grid

| | Value |
|---|---|
| Columns | **3** at ≥1280. Not 4 — a 4-column card at Studio's widths holds a title and two rows before it lies about how much fits |
| Gutter | `spacing.s24`, both axes — the same value as panel→panel in [metrics](metrics.md), because that is what it is |
| Column width | equal, `minmax(0, 1fr)`. Never content-driven: a grid whose columns resize as the data changes cannot be learned |
| Span | a card may span **2**; never 3, which is a section, not a card |
| Row alignment | `align-items: stretch` — cards in a row are the same height, and the footer link pins to the floor with `margin-top: auto` |
| Card count | **3 to 6**. Fewer is a detail screen; more is a screen with no opinion |

**One card may span 2, and it is the one with the most fields per row.** A row carrying four
facts (name, time, person, count) needs the width; a row carrying two does not. If two cards
both want the span, one of them has too many columns and belongs in a table.

## Card order is fixed and never reorders by urgency

The card an owner needs is in the same place every morning. This is the same law as
[stable column widths](tables.md#density-and-volume) and for the same reason: a layout that
changes shape with its data cannot be learned, and a dashboard is looked at every day by the
same person.

Urgency is expressed by **the alert slot** and by **pill tone inside a card** — never by
moving a card, resizing one, or colouring its whole surface.

## The alert slot — how a dashboard spends its budgets

This is the part of the shape that no other pattern has, and the part most likely to be got
wrong.

Every other screen in Konjo has a **fixed subject**, so the [four budgets](../konjo-design-language.md)
can be spent at design time: the designer decides which thing is loud, and it is the same
thing on every render. A dashboard's most important fact **changes daily** — on Tuesday an
uncovered class, on Wednesday three unsigned waivers, on Thursday nothing at all — and
loudness in this system is a property of a component, not of a fact.

So the emphasis is allocated to a **slot**, not to a card:

- **One alert slot**, directly under the page header, above the grid, full content width, in
  flow — never floating, never a toast. See [notifications](../patterns/notifications.md).
- **At most one alert renders**, ever. Two urgent things is one alert plus a pill.
- **It renders only when a stated bar is met.** What clears the bar is a
  [product fact](../product-facts.md), not a design judgement — "which costs the dojo more, an
  uncovered class or an unsigned waiver" is a business answer. If the bar for a surface is not
  written down, the slot renders only for facts whose urgency is unambiguous, and you name the
  missing bar rather than inventing a ranking.
- **On a quiet morning the slot is empty and the page has no loud element. That is correct.**
  The skill's closing bar — *if you cannot point at the one loud thing within a second, it
  isn't finished* — is asked of a screen with a fixed subject. Here, the absence of an alert
  **is** the answer to the screen's question, and it is a good answer. Never manufacture
  urgency to fill the slot: no "all clear" banner, no green celebration, no card promoted to
  loud because nothing else was.

| | |
|---|---|
| Fill | `feedback.errorFill`, or `feedback.warningFill` if the bar it cleared is a warning-tone fact |
| Border | 1px matching `feedback.*Border` — a fill alone is ~1.02:1 on the page and reads as a different background |
| Text | `caption` 13/400 in the matching `feedback.*Text` |
| Radius | `radius.card` 12 |
| Padding | `spacing.s16` |
| Action | **exactly one**, a text link — `label` 13/700 in `text.primary`, min-height `controlHeight.chip` 32 |

**The alert does not spend the red role.** `feedback.errorText` reports a fact; `brand.red`
invites an action. See [what a status means](metrics.md#what-a-status-means). A dashboard
normally spends **zero** of the red role, zero loud blocks and zero filled primaries, and
one uppercase register on the eyebrow. Zero is not a failure on this shape.

## The card

| | Token |
|---|---|
| Surface | `surface.card` on `surface.page`. Tone separates it; see [tables](tables.md#anatomy) |
| Radius | `radius.card` 12 — a Studio panel is a card, not a `radius.large` feature block |
| Border | none required. A hairline is an optical edge, never the separator |
| Shadow, gradient | never |
| Padding | `spacing.s16` |
| Title | `typography.title` 22/700, `text.primary` |
| Status line, beside the title | `caption` 13/400, `text.secondary`, right-aligned. Usually a count — "4 classes", both plural forms written out, the unit on **every** row that carries one. It may instead be the card's one framing fact where a count is not the answer: "Next test Sep 14". One or the other, never both |
| Title → first row | `spacing.s12` |
| Row → row | `spacing.s12` |
| Last row → footer link | `spacing.s16` |
| Footer link | `label` 13/700, `text.primary`, min-height `controlHeight.chip` 32, pinned with `margin-top: auto` |
| Arrow | `→` typed, or `iconSize.inline` 16 — one or the other on a screen, not both |

**The card is not a link.** Its rows and its footer link navigate; the card body does not.
Otherwise the same destination is reachable twice in one viewport and a keyboard user tabs
through a card-sized target with no name.

**Three cards, three shapes, and no fourth:**

### A row card

Rows of records. Subject `rowTitle` 16/600 `text.primary`; supporting line `caption` 13/400
`text.secondary`; min-height `controlHeight.chip` 32.

- **Five rows maximum**, then the footer link. A card is a sample, not a list.
- **At five or fewer, no hairlines** — 12pt of space groups them, per L3. Above five you are
  building a table in a box; move it.
- **The subject leads.** Never invert to put a time or a date first — an inverted row is
  [anti-pattern #12](../../../.claude/skills/design/references/anti-patterns.md).
- **One status pill per row, right-aligned.** More than one and the row has become a table
  row. Where a record genuinely has several reasons, show the highest tone and enumerate all
  of them in the accessible name.
- **Row actions are text links**, `label` 13/700, never buttons. Five bordered buttons down a
  card is five controls of equal weight competing with the footer link.

### A stat card

Label, value, hint. See [charts](charts.md) for when a number should be a chart instead.

- Label: `eyebrow`, `text.secondary`.
- Value: **`typography.metric` 28/800**, `text.primary`. Never `hero` — that is a loud block,
  counted per instance.
- Hint: `caption` 13/400. The comparison lives here ("+12 vs last month"), and its sign is the
  only place a `feedback.*` tone is legal on a stat card.
- **At most three stats in one card.** Four is a table.

### A mixed card

One stat on top, up to three rows under it. Used when the number needs naming — *"6
test-ready"* over the three students closest to it. The stat and the rows must be the **same
fact**; a number over unrelated rows is two cards pretending to be one.

## States

Each card loads, empties, and fails **independently**. A dashboard assembles from unrelated
queries, and one slow endpoint must not hold the other five.

### Loading

- Under 1s, nothing. See [states](../patterns/states.md).
- Then, per card: **the frame renders immediately** — title and footer link — and the body is
  `Skeleton.Text` at the line height of the token it stands in for. No pulse, no shimmer.
  This is the same rule as [a table's header rendering before its rows](tables.md#states).
- A dashboard is the daily landing screen, so it **always** qualifies for skeletons: the
  person has seen this layout before by definition.
- Skeletons are hidden from assistive tech; completion is announced once, politely, for the
  page — not six times, once per card.

### Empty — nothing today

One line of `body` 15/400 in `text.secondary`, left-aligned, in flow. **No action, no
illustration, never centred in a void.**

> No classes today. · Nobody needs attention today. · No birthdays in the next 30 days.

**The card still renders.** *(Shape-local. It overrides the detail-screen rule deliberately.)*
[states](../patterns/states.md) says a section with nothing to say does not render, and that
rule is written for a **detail screen**, whose sections vary by record. A dashboard's grid is fixed, and a card that disappears on a quiet day changes the
page's shape every morning — which is the thing *card order is fixed* exists to prevent. On
this shape, "nothing today" is an answer worth printing.

### Not yet set up — the third state

Empty means *nothing happened*. Not-yet-set-up means *this was never switched on*, and
reporting the first when the second is true is the design lying to the owner. A dashboard is
where the difference shows, because a new dojo's Home is mostly unconfigured.

One line of `body` 15/400, plus **one text link** — this is the actionable-emptiness exception:

> Lead capture isn't turned on. · `Set up your public page →`

Tell them what is off and where the switch is. Never dress it as data ("0 leads"), and never
as an error.

### Error — one card fails

The card keeps its title and footer link and replaces its body:

- `feedback.errorFill`, 1px `feedback.errorBorder`, `radius.card` 12, `spacing.s16` padding.
- One line of `caption` in `feedback.errorText`. No codes, no raw messages.
- **Retry is a bordered secondary**, `controlHeight.dense` 36 — a state's action inherits the
  weight of what it replaced, and what it replaced was a list, not a primary action.
- Every other card keeps rendering.

### Error — the page fails

Nothing else is on screen, so the retry **is** the filled primary: eyebrow, `title` 22/700,
one `body` line, one filled button at `controlHeight.primary` 48.

## Responsive

Studio's stated range is 1280–1440, and a laptop outside it is not a reason to invent a phone
layout.

| Viewport | Behaviour |
|---|---|
| ≥ 1440 | 3 columns; they grow, there is no max width |
| 1280–1439 | 3 columns |
| 1024–1279 | 2 columns; the spanning card spans both |
| < 1024 | 1 column; the rail collapses to icons with `aria-label`s and the cards keep their order |

The page never scrolls horizontally. A card that cannot fit its content at 1024 has too many
columns in its rows.

## Accessibility

The web contract in [accessibility](../foundations/accessibility.md#the-same-contract-on-the-web)
covers the general rules; these are the ones this shape needs.

- **Each card is a `<section>` with an `aria-labelledby` pointing at its `<h2>`**, so a screen
  reader can list the regions and jump between them. The count line is the section's
  `aria-describedby`.
- **`<h1>` is the page title, `<h2>` is every card title.** Do not skip to `<h3>` because a
  card looks smaller than a page.
- **The alert slot is not `role="alert"`.** It is present at page load, so nothing *changed* —
  `role="alert"` would interrupt the screen reader every single morning. It is a labelled
  `<section>` like the cards. `role="alert"` is for something that appears **after** an
  action.
- **A row action names its row**: `aria-label="Assign a sub for Adult Intermediate at 7:15pm"`,
  never bare "Assign".
- **A row with several status reasons enumerates all of them in its accessible name**, even
  when only the highest tone is drawn.
- **Rows are separate tab stops on the web.** Mobile's "group a row into one stop" is a
  VoiceOver rotor idiom; on the web a row with a link, a pill and a second link is correctly
  two stops, and collapsing it into one hides the second action from a keyboard.
- **A skip link is mandatory** on every Studio screen: the rail is ~25 links and comes before
  the content in DOM order on every page.
- Every pill carries its word — status is never colour alone.

## The self-check

- Can the owner name the one thing that needs them, in under a second, on a **busy** morning?
- On a **quiet** morning, does the page say so plainly rather than performing calm?
- Is every card in the same place it was yesterday?
- Does every card end in a doorway — a link to the place the work actually happens?
- Is any card doing a table's job? Move it.
