# Chassi do Horus BI — o que envolve o gráfico

## Conteúdo

- 1. Por que este arquivo existe
- 2. A fonte de verdade, e como reconferir
- 3. O grid da página
- 4. O card do widget (4 variantes)
- 5. O header: título e ações
- 6. A linha de KPI
- 7. A tabela
- 8. A barra de filtros
- 9. O registry de tipo de widget
- 10. CSS de referência, pronto para colar
- 11. O que este arquivo NÃO decide

Um gráfico Vega-Lite renderizado sozinho não parece o Horus BI. O que faz o
cliente reconhecer o produto dele é o que está **em volta** do gráfico: o card
branco com borda quase invisível, o título em slate-700 no canto superior
esquerdo, a lupa e o ícone de expandir em slate-300 no canto direito, a linha de
KPI no topo da tela, o grid de 12 colunas com gutter de 4px.

Este arquivo é o dono desse chassi. Leia ANTES de montar qualquer página de
mockup. O tema do gráfico em si é de
[`identidade-visual.md`](identidade-visual.md) e
[`horus-theme.json`](horus-theme.json); os tokens deste arquivo vivem em
[`horus-chassi.json`](horus-chassi.json).

## 1. Por que este arquivo existe

Até a reescrita de 2026-07, a skill produzia um render sóbrio e borderless que
vestia o manual de marca da Horus-empresa. O cliente assinava olhando aquilo e
recebia outra coisa: o Horus BI-produto, que tem card, borda, Poppins e uma
paleta de 11 cores.

O mockup existe para o cliente aprovar escopo. Ele aprova melhor quando a peça
tem a silhueta do que vai chegar. O chassi é a maior parte dessa silhueta, e era
justamente a parte que faltava.

## 2. A fonte de verdade, e como reconferir

Repositório `bi-monorepo-front`, branch **`main`**, app
`apps/horus.dataviz.frontend/`.

**Armadilha real, já vivida:** a primeira extração destes tokens pegou uma
branch de trabalho cujo `Configs.ts` e `tailwind.config.cjs` divergiam de
`main`. A paleta lida estava errada e a fonte também. Antes de confiar em
qualquer valor daqui:

```bash
cd ~/git/bi-monorepo-front
git rev-parse --abbrev-ref HEAD          # está em main?
git diff main --stat -- apps/horus.dataviz.frontend/src/components/Widgets/
```

Se a saída não for vazia, leia via `git show main:<caminho>`, nunca o arquivo do
working tree.

**Os prints de dashboard não são fonte de cor.** Os screenshots que circulam
internamente são de 2021 e mostram um tema já substituído (coral `#E84838`, azul
`#78B0E8`). Eles valem para layout, arquétipo e hierarquia. Para cor, só o
código.

| Token | Arquivo em `main` |
|---|---|
| paleta de série | `src/components/Widgets/Configs.ts` |
| card, header, ações | `src/components/Widgets/Widget.svelte` |
| grid | `src/components/Widgets/Container.svelte` |
| KPI | `src/components/Widgets/KpiV2.svelte` |
| eixo, legenda, data label | `src/components/Widgets/Barline.svelte` |
| donut | `src/components/Widgets/Pie.svelte` |
| cores nomeadas, fontes | `tailwind.config.cjs` |

## 3. O grid da página

`Container.svelte:65`, GridStack:

| Propriedade | Valor |
|---|---|
| colunas | 12 (configurável por dashboard via `widget_columns`, default 12) |
| `margin` | 2 (gutter visual de 4px entre widgets) |
| `cellHeight` | 40px |
| breakpoint mobile | `< 1024px` colapsa para 1 coluna |

A altura de um widget é `h × 40px`. Isso importa para o mockup porque define as
proporções reais:

| Widget | `h` típico | Altura |
|---|---|---|
| KPI | 2 | 80px |
| gráfico de linha ou barra | 6 | 240px |
| tabela | 6 a 10 | 240 a 400px |
| mapa | 8 a 12 | 320 a 480px |

Fundo da página: `#F8FAFC`. Este é o único token do chassi **inferido do print**
(amostrado `#F8F8F8`, arredondado para o `slate-50` do design system) porque o
grid herda o fundo do layout e não declara o próprio. Está marcado como tal em
`horus-chassi.json`.

## 4. O card do widget (4 variantes)

`Widget.svelte:439-465`. O campo que escolhe é `chart.visualType`.

### `default` (o que usar salvo instrução contrária)

```
rounded-lg bg-white box-border border-2 border-slate-100/50 shadow-sm
```

Traduzido: fundo branco, raio 8px, borda de **2px** em `rgba(241,245,249,0.5)`,
sombra `0 1px 2px 0 rgb(0 0 0 / 0.05)`.

A borda de 2px num cinza a 50% de opacidade é o detalhe que mais engana quem
tenta reproduzir de memória. Ela quase não se vê contra o branco, mas separa o
card do fundo `#F8FAFC` da página. Uma borda de 1px em cinza sólido já não
parece o produto.

### `transparent`

```
rounded-lg bg-slate-150 border-2
```

`slate-150` é um token custom do `tailwind.config.cjs`: `#E9EEF5`.

### `highlight`

Sem preenchimento próprio, só borda. Usado quando o widget carrega cor de fundo
semântica vinda do dado.

### `no_border`

Sem card. O gráfico sangra direto no fundo da página.

## 5. O header: título e ações

O título do widget **não fica dentro do canvas do gráfico**. Ele vive no header
do card, e o spec Vega-Lite do gráfico não deve emitir `title`.

`Widget.svelte:731`:

```
font-medium tracking-wide text-slate-700
```

`#334155`, peso 500, `letter-spacing: 0.025em`, ~14px.

As ações ficam em `absolute top-4 right-4` (`Widget.svelte:778`), em
`text-slate-300` (`#CBD5E1`) com hover `slate-400`. No produto são lupa
(pesquisar na tabela) e "abrir em nova aba" (relatório). No mockup elas entram
como **elemento de silhueta**, sem função, porque a presença delas é parte de
como o cliente reconhece a tela.

## 6. A linha de KPI

`KpiV2.svelte:1279-1341`. A estrutura é um grid de duas colunas:

```
grid grid-cols-[auto_1fr] items-center gap-3
```

Coluna 1 é a zona do ícone, coluna 2 é o conteúdo, empilhado:

| Parte | Estilo | Obrigatória? |
|---|---|---|
| overline | `text-xs uppercase tracking-wide opacity-70` | não |
| valor | `font-medium`, tamanho escalado ao container (20 a 48px) | sim |
| subtitle | `text-xs opacity-80` | não |
| helper | `text-[10px] opacity-60` | não |
| sparkline | linha de fundo ou inline | não |
| ring de progresso | `<circle>` com `stroke-width: 4` | não |

Nos prints de 2021 o ícone aparece à direita e o rótulo abaixo do valor. No
`KpiV2` atual o ícone está na coluna da esquerda. **Siga o `KpiV2`**, que é o
que o cliente vai receber.

O valor do KPI é magnitude. Ele recebe o **placeholder** da escala escolhida
(`R$ 123.456,78`), na tipografia escalada do slot. Regra completa e tabela de
escalas em [`forma-sem-magnitude.md`](forma-sem-magnitude.md) §2.

## 7. A tabela

A tabela é um dos widgets mais frequentes do produto e tem marcas próprias:

- coluna `#` com numeração sequencial, largura ~28px, alinhada à esquerda
- header sem fundo colorido, texto slate-700, borda inferior fina
- valor numérico alinhado à direita, formato brasileiro
- linha `Total` no rodapé quando o widget declara total
- scrollbar vertical quando estoura a altura, e o corte no meio de uma linha é
  parte da aparência real (uma tabela que termina exatamente na borda do card
  parece mockup, não produto)

Formato brasileiro, observado em todos os prints:

| Tipo | Forma |
|---|---|
| moeda | `R$ 1.234.567,89` |
| percentual | `12,3%` |
| inteiro | `1.007.800` |
| duração | `1m 19s`, `9h 46m`, `1d 1h 18m` |
| data em célula | `14/04/2021` |
| data em eixo | `14 Abr 2021` |
| eixo abreviado | `25k`, `1M`, `8M` |

O **formato** é forma e aparece por inteiro. A **variação entre valores** é
magnitude e some: toda linha da coluna mostra o mesmo placeholder, e a linha
`Total` mostra o placeholder de uma escala acima
([`forma-sem-magnitude.md`](forma-sem-magnitude.md) §2c).

## 8. A barra de filtros

Uma barra só, no canto superior esquerdo: caixas brancas com label pequeno em
`#949DAC` na primeira linha e o valor selecionado embaixo (`Todas selecionadas`,
`Todos selecionados`), mais um ícone de limpar.

> **A data é um filtro como os outros, e vem sempre primeiro.**

Isso corrige uma leitura errada dos prints. Neles o seletor de período aparece
como um pill isolado no canto superior **direito**, e a primeira versão deste
arquivo copiou esse arranjo. Está errado: no modelo do produto o período é mais
uma dimensão de filtro, mora na mesma barra de pills à esquerda, e ocupa a
**primeira** posição. Não existe pill de período separado.

Ordem da barra, então:

1. **Período** (`🗓 Abril 1, 2026 - Abril 30, 2026 ▾`)
2. as demais dimensões, na ordem em que a ficha as declara (`Filial`,
   `Vendedor`, `Produto`…)

A barra entra no mockup porque enquadra a tela inteira e é a primeira coisa que
o cliente reconhece. Os rótulos devem usar as dimensões reais da ficha, o que
transforma o chassi em mais um lugar onde o escopo fica visível: se a tela filtra
por filial e vendedor, o mockup mostra `Filial` e `Vendedor`, e o cliente pode
discordar disso antes de assinar.

## 9. O registry de tipo de widget

`Widget.svelte:50-67`. Toda ficha de mockup precisa aterrissar em um destes:

| Tipo | O que é |
|---|---|
| `Kpi` | cartão de número único, com ícone, sparkline e ring opcionais |
| `Barline` | barra (vertical ou horizontal), linha, área, combo, eixo Y duplo |
| `Pie` | pizza e donut (`innerSize: "60%"`) |
| `Table` | tabela com coluna `#`, total, heatmap de célula |
| `Matrix` | pivot com grupos de coluna, hierarquia e subtotal |
| `Map` | Leaflet: choropleth contínuo, polígono categórico, pino |
| `Gauge` | velocímetro de meta com ponteiro |
| `Punchcard` | densidade em grade (dia × hora) |
| `Calendar` | heatmap de calendário |
| `Diagram` | fluxo e relação |
| `Forecast` | série com projeção |
| `ListBox` | lista de seleção que também filtra |
| `Text` | bloco de texto |
| `App` | widget que embute outra app |
| `Svelte` / `ParametricSvelte` | widget sob medida |

Um widget de mockup que não cai em nenhum desses nomes é escopo que a plataforma
não entrega hoje. Marque `custom: true` na ficha e sinalize o custo cedo, como
[`ficha-para-vega.md`](ficha-para-vega.md) já manda para `fluxo`.

## 10. CSS de referência, pronto para colar

Derivado dos tokens de `horus-chassi.json`. O gerador do
[`wireframe-kit.md`](wireframe-kit.md) emite isto; a listagem existe aqui para
conferência visual.

```css
:root {
  --hb-page:        #F8FAFC;
  --hb-card:        #FFFFFF;
  --hb-card-border: rgba(241, 245, 249, 0.5);
  --hb-title:       #334155;
  --hb-action:      #CBD5E1;
  --hb-axis:        #949DAC;
  --hb-grid:        #efefef;
  --hb-slab:        #E2E8F0;
  --hb-cell-border: #F1F5F9;
}

body {
  margin: 0;
  padding: 8px;
  background: var(--hb-page);
  font-family: Poppins, system-ui, sans-serif;
}

.hb-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 4px;                      /* GridStack margin: 2 */
  grid-auto-rows: 40px;          /* cellHeight */
}

.hb-card {
  position: relative;
  background: var(--hb-card);
  border: 2px solid var(--hb-card-border);
  border-radius: 8px;
  box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  padding: 16px;
  overflow: hidden;
}

.hb-card--transparent { background: #E9EEF5; }
.hb-card--no-border   { background: transparent; border: 0; box-shadow: none; }

.hb-title {
  color: var(--hb-title);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.025em;
  margin: 0 0 8px;
}

.hb-actions {
  position: absolute;
  top: 16px;
  right: 16px;
  display: flex;
  gap: 8px;
  color: var(--hb-action);
}

/* KPI — KpiV2.svelte */
.hb-kpi        { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 12px; }
.hb-kpi__over  { font-size: 12px; text-transform: uppercase; letter-spacing: .025em; opacity: .7; }
.hb-kpi__sub   { font-size: 12px; opacity: .8; }
.hb-kpi__help  { font-size: 10px; opacity: .6; }

/* Tabela */
.hb-table            { width: 100%; border-collapse: collapse; font-size: 12px; color: var(--hb-title); }
.hb-table th         { font-weight: 500; text-align: left; border-bottom: 1px solid #E2E8F0; padding: 4px 6px; }
.hb-table td         { border-bottom: 1px solid var(--hb-cell-border); padding: 4px 6px; }
.hb-table .num       { text-align: right; }
.hb-table .ord       { width: 28px; color: var(--hb-axis); }
.hb-table tfoot td   { font-weight: 600; border-top: 1px solid #E2E8F0; border-bottom: 0; }

/* Barra de filtros. A DATA é o primeiro pill, sempre. Ver §8. */
.hb-filters { display: flex; gap: 6px; margin-bottom: 8px; }
.hb-filter  { background: #fff; border: 1px solid #E2E8F0; border-radius: 4px;
              padding: 4px 8px; font-size: 12px; }
.hb-filter b { display: block; font-size: 10px; font-weight: 400; color: var(--hb-axis); }

/* Slab — fallback, só quando não há escala honesta. Ver forma-sem-magnitude.md §2b */
.hb-slab {
  display: inline-block;
  height: 12px;
  border-radius: 5px;
  background: var(--hb-slab);
  vertical-align: middle;
}
```

O número em si não precisa de classe: ele é texto normal, no formato da tabela
de escalas, dentro do slot que já existe (`.hb-kpi__val`, `td.num`, ou o
`mark: "text"` do spec).

## 11. O que este arquivo NÃO decide

- **Qual gráfico cada `relacao` vira.** É de
  [`ficha-para-vega.md`](ficha-para-vega.md).
- **Qual placeholder cada número recebe.** É de
  [`forma-sem-magnitude.md`](forma-sem-magnitude.md).
- **Cor de série e regra das 5 séries.** É de
  [`identidade-visual.md`](identidade-visual.md).
- **A estética do dashboard entregue com dado real.** É da skill `lumo-design`,
  depois da assinatura.
- **Dark mode.** Os tokens existem em `horus-chassi.json` sob a chave `dark`,
  sem rota de render. O mockup de proposta sai só em claro.
