# Build the "Composed dashboard" pattern: `ComposedDashboard`

## Stack (edit this block to match your project)
- React 18+ with TypeScript: one function component, typed props, no `any`.
- Tailwind CSS, using your design system's tokens if you have them.
- Presentational only: data comes in through props, decisions go out through callbacks. No fetching or global state inside.
- If your codebase already has conventions (naming, shared components, tokens), follow them over this spec.

## What the pattern is for
The agent assembles a layout of charts and panels answering the question just asked.

Use it when: Analytics and monitoring where the useful view differs with every question.

## What to build
A grid of panels the agent assembled to answer one question, built from a layout spec that can be saved, re-run and shared. Every panel shows the query behind it, which a person can read and edit. It renders in an analytics workspace below the question.

### Props
- `question`: string, shown at the top.
- `layout`: { id?: string; generatedAt: Date; sources: string[]; panels: Array<{ id: string; type: "kpi" | "bar" | "line" | "table" | "text"; title: string; query: { text: string; language: string }; state: "loading" | "ready" | "empty" | "error" | "stale"; error?: string }> }, the persistable spec.
- `renderPanel(panel)`: (panel) => ReactNode, draws the chart or table; the dashboard owns layout and chrome only.
- `saved?`: { id: string; name: string }, set once the layout is saved.
- `savedViews?`: Array<{ id: string; name: string }>, plus `onOpenSaved(id)`.
- `onSave(name: string)`, `onEditQuery(panelId, query)`, `onRemovePanel(panelId)` and `onRerun()`: callbacks. The component never queries data itself.

### States it must render
- Assembling: one skeleton per panel in the spec, in final positions.
- Ready: panels with values, and a provenance line such as "Built 4 panels from billing and onboarding data, 1 Oct 09:12" in the AI accent.
- Query open: a panel's query is visible in a code block with Edit.
- Stale panel: after a query edit, the panel shows "Query changed, run to update" until re-run.
- Panel error or empty: that panel says why ("No rows", "No access to onboarding") with its query; other panels are unaffected.
- Saved: "Saved as Q3 churn drivers" and the view appears in the saved list, re-runnable.

### Interactions
- Each panel has a Query disclosure; Edit turns it into a text area, and Run calls `onEditQuery`.
- Save view asks for a name (prefilled from the question) and calls `onSave`.
- Remove panel calls `onRemovePanel`; Re-run calls `onRerun` and keeps the layout unchanged.

### Edge cases
- Re-running a saved view refreshes data and timestamp but never re-generates the layout.
- A panel whose query references data the person can't access shows a permission message, not an empty chart.
- Under 640 px panels stack in spec order, KPIs first.
- A layout with no panels: show the question with "No panels were produced" and Re-run.

## Must avoid
Generating a fresh throwaway layout every time, with no way to pin, save, or share the good one.

## Engineering constraint
Emit a layout spec you can persist and re-run; keep the underlying query visible and editable.

## Accessibility
- Each panel is a `region` labelled by its title; every chart has a short text summary or table alternative from `renderPanel`.
- Announce when all panels finish loading, not each one.

## House rules
- Real `<button>`/`<input>` elements, full keyboard use, visible focus; announce status changes with `aria-live="polite"`; respect `prefers-reduced-motion`.
- All user-facing text comes from props (or a `labels` prop) so it can be translated.
- One consistent accent marks anything the AI made or is doing; amber means "needs a person", red means risky, green means done. Never rely on colour alone.
- Fits a 360 px wide container without horizontal scrolling.

## Acceptance checks
- [ ] Every state above can be rendered by passing props, with no internal fake data.
- [ ] Every interaction and edge case above behaves as described.
- [ ] Nothing in "Must avoid" can happen with this component.

## Deliver
1. The component file with its exported prop types.
2. A short usage example that renders each state.
