@snksergio/design-system 0.61.0 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/ai/componentes/AlertModal.md +47 -0
- package/dist-lib/ai/componentes/AppShell.md +117 -0
- package/dist-lib/ai/componentes/Breadcrumb.md +187 -0
- package/dist-lib/ai/componentes/Button.md +116 -0
- package/dist-lib/ai/componentes/ButtonGroup.md +162 -0
- package/dist-lib/ai/componentes/CardCheckbox.md +99 -0
- package/dist-lib/ai/componentes/CardOption.md +133 -0
- package/dist-lib/ai/componentes/Chart.md +93 -0
- package/dist-lib/ai/componentes/Chip.md +68 -0
- package/dist-lib/ai/componentes/ChoroplethMap.md +118 -0
- package/dist-lib/ai/componentes/ColorPicker.md +70 -0
- package/dist-lib/ai/componentes/Combobox.md +51 -0
- package/dist-lib/ai/componentes/ConversationListItem.md +90 -0
- package/dist-lib/ai/componentes/DataList.md +111 -0
- package/dist-lib/ai/componentes/DataTable.md +867 -0
- package/dist-lib/ai/componentes/DatePicker.md +84 -0
- package/dist-lib/ai/componentes/DateSeparatorChip.md +59 -0
- package/dist-lib/ai/componentes/EmptyState.md +72 -0
- package/dist-lib/ai/componentes/FileUploadField.md +95 -0
- package/dist-lib/ai/componentes/FloatingPanel.md +118 -0
- package/dist-lib/ai/componentes/FooterTable.md +62 -0
- package/dist-lib/ai/componentes/FormField.md +110 -0
- package/dist-lib/ai/componentes/Gantt.md +552 -0
- package/dist-lib/ai/componentes/Header.md +98 -0
- package/dist-lib/ai/componentes/Icon.md +65 -0
- package/dist-lib/ai/componentes/Kanban.md +343 -0
- package/dist-lib/ai/componentes/Kpi.md +103 -0
- package/dist-lib/ai/componentes/List.md +61 -0
- package/dist-lib/ai/componentes/MarkdownText.md +59 -0
- package/dist-lib/ai/componentes/MenuSidebar.md +128 -0
- package/dist-lib/ai/componentes/MessageAck.md +53 -0
- package/dist-lib/ai/componentes/MessageBubble.md +115 -0
- package/dist-lib/ai/componentes/MessageComposer.md +80 -0
- package/dist-lib/ai/componentes/MessageVariablesPicker.md +104 -0
- package/dist-lib/ai/componentes/Modal.md +88 -0
- package/dist-lib/ai/componentes/MonthYearPicker.md +49 -0
- package/dist-lib/ai/componentes/PageHeader.md +129 -0
- package/dist-lib/ai/componentes/Panel.md +84 -0
- package/dist-lib/ai/componentes/Scheduler.md +421 -0
- package/dist-lib/ai/componentes/ScreenLoader.md +60 -0
- package/dist-lib/ai/componentes/SingleMenuSidebar.md +171 -0
- package/dist-lib/ai/componentes/Spinner.md +52 -0
- package/dist-lib/ai/componentes/Table.md +192 -0
- package/dist-lib/ai/componentes/TableToolbar.md +87 -0
- package/dist-lib/ai/componentes/TabsNavigation.md +152 -0
- package/dist-lib/ai/componentes/Toast.md +49 -0
- package/dist-lib/ai/componentes/_primitivos.md +74 -0
- package/dist-lib/ai/componentes/avatar-ig.md +181 -0
- package/dist-lib/ai/componentes/indice.json +49 -0
- package/dist-lib/ai/exemplos/dashboard/dashboard-brazil-map.ts +33 -0
- package/dist-lib/ai/exemplos/dashboard/dashboard-screen.tsx +1110 -0
- package/dist-lib/ai/exemplos/dashboard/index.ts +1 -0
- package/dist-lib/ai/global/componentes.md +217 -0
- package/dist-lib/ai/global/composicao.md +182 -0
- package/dist-lib/ai/indice.json +177 -0
- package/dist-lib/ai/lint/ds-lint-patterns.mjs +115 -0
- package/dist-lib/ai/manifest.json +16 -0
- package/dist-lib/ai/regras/design.md +88 -0
- package/dist-lib/ai/regras/temas.md +192 -0
- package/dist-lib/ai/regras-por-componente.json +103 -0
- package/dist-lib/ai/roteiros/dashboard/blueprint.md +47 -0
- package/dist-lib/ai/roteiros/dashboard/entrevista.md +62 -0
- package/dist-lib/ai/roteiros/dashboard/geracao.md +88 -0
- package/dist-lib/ai/roteiros/dashboard/roteiro.md +88 -0
- package/package.json +4 -1
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
# Gantt
|
|
2
|
+
|
|
3
|
+
<!-- ds:regras
|
|
4
|
+
- monte a tela a partir do `example-gantt` (`igreen:add example-gantt`) — é o comportamento COMPLETO, não um toy
|
|
5
|
+
- o pai precisa ter ALTURA (o componente é `h-full`): sem isso você vê só a toolbar
|
|
6
|
+
- busca do servidor → passe `loading`, senão `rows={[]}` afirma "Nenhuma tarefa neste período"
|
|
7
|
+
- `colorKey` diz CATEGORIA (qual frente), não status: status vai em `row.trailing` como `Chip`
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
Cronograma de projeto: hierarquia de tarefas à esquerda, tempo à direita, e
|
|
11
|
+
**vínculos** entre as barras. Categoria: data-display.
|
|
12
|
+
|
|
13
|
+
## Quando usar
|
|
14
|
+
|
|
15
|
+
Quando a pergunta é **"o que depende de quê"**. Sem vínculo isto é uma timeline,
|
|
16
|
+
não um Gantt — e o DS já tem timeline.
|
|
17
|
+
|
|
18
|
+
| Precisa de | Use |
|
|
19
|
+
|---|---|
|
|
20
|
+
| escolher **uma data** num formulário | `DatePicker` · `Calendar` · `MonthYearPicker` |
|
|
21
|
+
| mostrar **quando** algo acontece (compromisso, reserva, agenda) | `Scheduler` |
|
|
22
|
+
| mostrar **o que bloqueia o quê**, com hierarquia e progresso | **`Gantt`** |
|
|
23
|
+
| linhas de registro sem eixo de tempo | `DataTable` · `DataList` |
|
|
24
|
+
|
|
25
|
+
## Import
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { Gantt } from "@/components/ui/Gantt";
|
|
29
|
+
import type { GanttRow, GanttLink, GanttColumn } from "@/components/ui/Gantt";
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## ⭐ A referência é o `example-gantt`, não este arquivo
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx igreen:add example-gantt
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Antes de montar uma tela com o `Gantt`, comece por aqui.** O `example-gantt` é a
|
|
39
|
+
extração 1:1 do showcase `#/gantt-full` do DS — não é um toy, é a tela que valida
|
|
40
|
+
o componente a fundo, e ela é a fonte de verdade do **comportamento**. Este
|
|
41
|
+
`USAGE.md` descreve a API; o exemplo mostra como as peças se comportam **juntas**,
|
|
42
|
+
que é onde as decisões erradas aparecem.
|
|
43
|
+
|
|
44
|
+
O que ele exercita, e que um exemplo mínimo não mostra:
|
|
45
|
+
|
|
46
|
+
| | |
|
|
47
|
+
|---|---|
|
|
48
|
+
| **dado real** | 20 linhas, 4 níveis de hierarquia, `summary` derivado, marco, progresso |
|
|
49
|
+
| **grafo real** | 16 vínculos dos 4 tipos com `lag`, **2 conflitos** (um deles não foi plantado — as datas foram escritas e o componente achou) e caminho crítico |
|
|
50
|
+
| **os 4 gestos aplicando** | `rows`/`links` são estado da tela: arrastar move e a barra **fica**; criar e remover vínculo funcionam |
|
|
51
|
+
| **as 3 visões** | o mesmo dado em `timeline`, `calendar` e `list` |
|
|
52
|
+
| **filtro nos 6 `kind`** | com painel lateral e chips de aplicado |
|
|
53
|
+
| **o que é DA TELA** | painel de detalhe (`onBarClick`) e drawer de nova tarefa (`onDayAdd`) — o componente só emite |
|
|
54
|
+
|
|
55
|
+
⚠️ **O ponto mais fácil de errar** está lá resolvido: `draggable`/`resizable`
|
|
56
|
+
ligados **sem handler que aplique** fazem o usuário arrastar e ver a barra
|
|
57
|
+
**voltar**. No exemplo os handlers reescrevem o estado — copie essa parte.
|
|
58
|
+
|
|
59
|
+
O arquivo `_gantt-data.tsx` é o que você troca primeiro; `gantt-screen.tsx` tem um
|
|
60
|
+
cabeçalho "Cuidado ao adaptar" dizendo o que ligar ao seu estado, o que remover se
|
|
61
|
+
não servir e o que **não** mexer.
|
|
62
|
+
|
|
63
|
+
## Exemplo mínimo
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<Gantt
|
|
67
|
+
rows={[
|
|
68
|
+
{ id: "fase", label: "Descoberta", type: "summary", bars: [] },
|
|
69
|
+
{
|
|
70
|
+
id: "t1", label: "Entrevistas", parent: "fase",
|
|
71
|
+
bars: [{ id: "b1", label: "Entrevistas", start: d1, end: d2,
|
|
72
|
+
colorKey: "chart-1", progress: 100 }],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "t2", label: "Escopo", parent: "fase",
|
|
76
|
+
bars: [{ id: "b2", label: "Escopo", start: d2, end: d3, colorKey: "chart-1" }],
|
|
77
|
+
},
|
|
78
|
+
]}
|
|
79
|
+
links={[{ id: "v1", source: "b1", target: "b2", type: "FS" }]}
|
|
80
|
+
searchable
|
|
81
|
+
locale={ptBR}
|
|
82
|
+
onBarClick={(bar, row) => abrirDetalhe(row.id)}
|
|
83
|
+
/>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Props essenciais
|
|
87
|
+
|
|
88
|
+
| Prop | Tipo | Default |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `rows` | `GanttRow[]` | — |
|
|
91
|
+
| `links` | `GanttLink[]` — ausente = sem setas | — |
|
|
92
|
+
| `views` | `GanttView[]` — **quais visões existem** | as três |
|
|
93
|
+
| `view` | `"timeline" \| "calendar" \| "list"` — qual está **aberta** | `"timeline"` |
|
|
94
|
+
| `toolbarActions` / `primaryAction` | `ReactNode` — os dois slots livres da toolbar | — |
|
|
95
|
+
| `emptyState` | `ReactNode` — substitui o vazio de busca/filtro | ícone + texto |
|
|
96
|
+
| `loading` / `loadingState` | `boolean` + `ReactNode` — esqueleto no lugar do conteúdo | `false` |
|
|
97
|
+
| `windowStart` / `windowEnd` | `Date` — **do consumidor** | derivada dos dados |
|
|
98
|
+
| `granularity` | `"day" \| "week" \| "month" \| "quarter"` | `"day"` |
|
|
99
|
+
| `onViewChange` / `onGranularityChange` / `onCriticalPathChange` | os pares dos três estados de UI — ver abaixo | — |
|
|
100
|
+
| `columns` | `GanttColumn[]` | nome + início + fim |
|
|
101
|
+
| `gridWidth` | `number` — largura inicial; o divisor é arrastável | `360` |
|
|
102
|
+
| `draggable` / `resizable` / `linkable` | `boolean` | **`false`** |
|
|
103
|
+
| `criticalPath` | `boolean` — liga o realce | **`false`** |
|
|
104
|
+
| `criticalPathToggle` | `boolean` — mostra o botão "Crítico" na toolbar | `true` |
|
|
105
|
+
| `onBarClick` | `(bar, row, evt) => void` | — |
|
|
106
|
+
| `onBarMove` / `onBarResize` | `(change) => void` — **emite, não aplica** | — |
|
|
107
|
+
| `onLinkViolations` | `(violations) => void` | — |
|
|
108
|
+
| `onGraphError` | `({ kind: "cycle", barIds }) => void` | — |
|
|
109
|
+
|
|
110
|
+
`GanttBar`: `{ id, start, end, label?, searchText?, colorKey?, progress?, continuesBefore?, continuesAfter?, meta? }`
|
|
111
|
+
`GanttLink`: `{ id, source, target, type?, lag? }`
|
|
112
|
+
`GanttFilterField`: `{ id, label, kind?, options?, accessor, searchable?, placeholder? }`
|
|
113
|
+
|
|
114
|
+
### Filtro — os 6 `kind`
|
|
115
|
+
|
|
116
|
+
O vocabulário espelha o da `DataTable`, pra o usuário ler a mesma frase nas duas
|
|
117
|
+
telas: *"Status é Ativo"*, *"Duração entre 3 e 10"*, *"Início a partir de 01/09/26"*.
|
|
118
|
+
|
|
119
|
+
| `kind` | Controle no painel | `filterModel[id]` | Operador no chip |
|
|
120
|
+
|---|---|---|---|
|
|
121
|
+
| `multi` *(default)* | checkboxes + busca (≥7 opções) + "selecionar todas" | valores marcados | `é` |
|
|
122
|
+
| `single` | radio | um valor | `é` |
|
|
123
|
+
| `text` | um campo de texto | `[termo]` | `contém` |
|
|
124
|
+
| `number` | dois campos numéricos | `[min, max]` — `""` = livre | `entre` / `≥` / `≤` |
|
|
125
|
+
| `date` | dois campos de data | `[de, até]` — `""` = livre | `entre` / `a partir de` / `até` |
|
|
126
|
+
| `boolean` | radio Sim/Não (ou seus `options`) | `["true"]` \| `["false"]` | `é` |
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
const FILTROS: GanttFilterField[] = [
|
|
130
|
+
{ id: "frente", label: "Frente", options: [...], accessor: r => meta(r).frente },
|
|
131
|
+
{ id: "desc", label: "Descrição", kind: "text", accessor: r => meta(r).descricao },
|
|
132
|
+
{ id: "dur", label: "Duração", kind: "number", accessor: r => dias(r) },
|
|
133
|
+
{ id: "inicio", label: "Início", kind: "date", accessor: r => r.bars[0]?.start },
|
|
134
|
+
{ id: "ok", label: "Concluída", kind: "boolean", accessor: r => r.bars[0]?.progress === 100 },
|
|
135
|
+
];
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
⚠️ **`GanttFilterModel` não mudou de forma** — segue `Record<string, string[]>`. Os 6
|
|
139
|
+
tipos codificam o valor em `string[]`, então `filterModel` controlado, persistência e
|
|
140
|
+
"Limpar tudo" que você já tinha continuam funcionando **sem migração**.
|
|
141
|
+
|
|
142
|
+
## Gotchas / cuidados
|
|
143
|
+
|
|
144
|
+
### 1. O pai precisa ter altura
|
|
145
|
+
|
|
146
|
+
O componente é `h-full`. Sem altura no pai, ele colapsa e você vê só a toolbar.
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
<div className="flex h-[520px] flex-col"> {/* ou h-full num pai com altura */}
|
|
150
|
+
<Gantt … />
|
|
151
|
+
</div>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 2. `links` referencia id de BARRA, não de linha
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
rows: { id: "t1", bars: [{ id: "b1", … }] }
|
|
158
|
+
links: { source: "b1", target: "b2" } // ✅ id da barra
|
|
159
|
+
links: { source: "t1", target: "t2" } // ❌ silenciosamente ignorado
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Uma linha-contêiner tem N barras, e o vínculo é entre trabalhos. Vínculo cujas
|
|
163
|
+
pontas não existem é **ignorado**, não reportado como erro — referência pendente
|
|
164
|
+
acontece em paginação e em edição otimista, e tratar como conflito encheria a
|
|
165
|
+
tela de falso positivo transitório.
|
|
166
|
+
|
|
167
|
+
### 3. O componente NÃO reagenda
|
|
168
|
+
|
|
169
|
+
Arrastar emite `onBarMove`. Vínculo violado emite `onLinkViolations` com o
|
|
170
|
+
déficit em dias. **Nada se move sozinho.**
|
|
171
|
+
|
|
172
|
+
Corrigir cronograma é decisão de negócio: mover a tarefa que atrasou, cortar
|
|
173
|
+
escopo, aceitar o atraso ou renegociar o vínculo são quatro respostas diferentes
|
|
174
|
+
pro mesmo conflito. Datas reescritas sozinhas parecem dados, e o erro seria
|
|
175
|
+
invisível.
|
|
176
|
+
|
|
177
|
+
### 4. A janela é do consumidor
|
|
178
|
+
|
|
179
|
+
`windowStart`/`windowEnd` vêm por prop, e as setas `‹ ›` **não fazem nada**
|
|
180
|
+
quando você as controla — mover é sua decisão. Omitidas, o componente mantém a
|
|
181
|
+
janela em estado próprio, derivada dos dados na primeira montagem.
|
|
182
|
+
|
|
183
|
+
### 5. `progress: undefined` ≠ `progress: 0`
|
|
184
|
+
|
|
185
|
+
Ausente = "não rastreia progresso", e não desenha trilha nenhuma.
|
|
186
|
+
`0` = "rastreia, e está em zero", e desenha a trilha vazia. Os dois estados
|
|
187
|
+
aparecem lado a lado em cronograma real.
|
|
188
|
+
|
|
189
|
+
### 6. A cor diz CATEGORIA, não estado
|
|
190
|
+
|
|
191
|
+
`colorKey` usa a paleta de chart (`chart-1`…`chart-5`) porque no Gantt a cor diz
|
|
192
|
+
**qual frente**. Status vai em `row.trailing` como `Chip`:
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
{ id: "fase", label: "Design", type: "summary", bars: [],
|
|
196
|
+
trailing: <Chip size="sm" variant="soft" color="warning">Em andamento</Chip> }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Azul (`chart-3`) é legítimo aqui: o `DESIGN.md` proíbe azul na **interface**, e o
|
|
200
|
+
`Chart/USAGE.md` o inclui como **dado categórico**. Barra de Gantt é dado.
|
|
201
|
+
|
|
202
|
+
### 7. A barra é tingida, não sólida com texto branco
|
|
203
|
+
|
|
204
|
+
Se você vier de uma referência com barra saturada e texto branco: o DS não faz
|
|
205
|
+
isso, e a razão foi **medida** — texto branco ou colorido sobre pílula tingida dá
|
|
206
|
+
contraste de **1.72–4.49 no light**, e nenhuma família passa AA. A cor viva vive
|
|
207
|
+
no acento da borda esquerda e no preenchimento de progresso; o texto é
|
|
208
|
+
`fg-default`.
|
|
209
|
+
|
|
210
|
+
### 8. `summary` deriva o intervalo dos filhos
|
|
211
|
+
|
|
212
|
+
`type: "summary"` com `bars: []` calcula o intervalo de **toda** a descendência
|
|
213
|
+
(não só filhos diretos). Passar `bars` num `summary` vence o cálculo — serve pra
|
|
214
|
+
quem tem o agregado do servidor.
|
|
215
|
+
|
|
216
|
+
O intervalo é derivado de **todas** as linhas, não das visíveis: colapsar não
|
|
217
|
+
encolhe a barra do summary, porque colapsar é justamente quando ela passa a ser
|
|
218
|
+
a única informação.
|
|
219
|
+
|
|
220
|
+
### 9. Caminho crítico só considera `FS`
|
|
221
|
+
|
|
222
|
+
Limite conhecido e declarado. Os outros três tipos exigem tratar as duas pontas
|
|
223
|
+
como nós independentes no grafo, e implementar isso pela metade daria caminho
|
|
224
|
+
crítico **plausível e errado** — que é pior que não ter. `SS`, `FF` e `SF`
|
|
225
|
+
continuam sendo validados como conflito; só não entram no cálculo de criticidade.
|
|
226
|
+
|
|
227
|
+
Ciclo no grafo → `onGraphError` e o crítico simplesmente não pinta. Ciclo é dado
|
|
228
|
+
do consumidor, não exceção do componente.
|
|
229
|
+
|
|
230
|
+
### 10. Uma altura de linha, duas superfícies
|
|
231
|
+
|
|
232
|
+
`GANTT_ROW_HEIGHT_PX` (48px) é constante e é consumida pelos **dois** painéis. Não
|
|
233
|
+
tente sobrescrever por CSS num deles: o desalinho entre nome e barra é o pior
|
|
234
|
+
defeito possível aqui, porque produz leitura errada sem parecer quebrado.
|
|
235
|
+
|
|
236
|
+
### 11. Trocar de escala nunca esconde trabalho
|
|
237
|
+
|
|
238
|
+
Quando o consumidor não controla `windowStart`/`windowEnd`, trocar a escala faz
|
|
239
|
+
duas coisas:
|
|
240
|
+
|
|
241
|
+
1. **A janela** vira a UNIÃO da largura própria daquela escala (60 dias em
|
|
242
|
+
`day`, 1825 em `quarter`) com a extensão real do cronograma. União e não
|
|
243
|
+
`max` porque ela é idempotente: `day → quarter → day` devolve exatamente a
|
|
244
|
+
janela original. Antes a janela era só a largura da escala, e voltar de
|
|
245
|
+
`quarter` pra `day` recortava em 60 dias — as barras das duas pontas
|
|
246
|
+
desapareciam sem nada dizer que havia mais.
|
|
247
|
+
2. **A viewport** recentra no mesmo instante do tempo que estava no meio da
|
|
248
|
+
tela. Sem isso, ir pra `quarter` deixava você olhando o começo de uma janela
|
|
249
|
+
de 5 anos cujos dados vivem no meio dela: canvas vazio.
|
|
250
|
+
|
|
251
|
+
Com `windowStart`/`windowEnd` controlados, nada disso acontece: a janela é sua,
|
|
252
|
+
e o componente não sobrescreve a sua decisão.
|
|
253
|
+
|
|
254
|
+
### 12. O gesto EMITE, você aplica
|
|
255
|
+
|
|
256
|
+
`draggable` / `resizable` / `linkable` ligam os gestos:
|
|
257
|
+
|
|
258
|
+
| Gesto | Onde | Emite |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| mover | `pointerdown` no corpo da barra | `onBarMove` |
|
|
261
|
+
| redimensionar | punho de 8px em cada ponta | `onBarResize` |
|
|
262
|
+
| criar vínculo | porta redonda fora da ponta → soltar sobre outra barra | `onLinkCreate` |
|
|
263
|
+
| remover vínculo | clique na seta | `onLinkDelete` |
|
|
264
|
+
|
|
265
|
+
⚠️ **Ligar sem handler = arrastar e ver voltar.** A prévia acontece (a barra
|
|
266
|
+
acompanha o ponteiro com snap de dia), mas nada persiste: o `Gantt` é dumb sobre
|
|
267
|
+
mutação, igual ao `Kanban`. Datas reescritas sozinhas parecem dados, e o erro
|
|
268
|
+
seria invisível.
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
const [rows, setRows] = useState(SEMENTE);
|
|
272
|
+
|
|
273
|
+
<Gantt
|
|
274
|
+
rows={rows}
|
|
275
|
+
draggable resizable linkable
|
|
276
|
+
onBarMove={({ bar, start, end }) => setRows(atualizar(bar.id, start, end))}
|
|
277
|
+
onBarResize={({ bar, start, end }) => setRows(atualizar(bar.id, start, end))}
|
|
278
|
+
onLinkCreate={(novo) => setLinks((l) => [...l, { ...novo, id: gerarId() }])}
|
|
279
|
+
/>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**O tipo do vínculo sai das duas pontas** — `linkTypeFromSides`: sair da ponta
|
|
283
|
+
direita e soltar na metade **esquerda** do destino dá `FS` (o caso comum); as
|
|
284
|
+
outras três combinações dão `SS`, `FF` e `SF`. O alvo é **meia barra**, não a
|
|
285
|
+
porta de 9px do destino: 9px não é afordância.
|
|
286
|
+
|
|
287
|
+
⛔ **Linha `summary` não aceita gesto.** O intervalo dela é derivado dos filhos e
|
|
288
|
+
a barra é sintética (id `<rowId>__summary`, que não existe no seu `rows`) — um
|
|
289
|
+
gesto ali emitiria uma `bar` que você não possui e um `source` pendurado. Mover
|
|
290
|
+
a fase é mover os filhos, e isso é decisão sua.
|
|
291
|
+
|
|
292
|
+
**Snap em dia inteiro**, sempre. Arraste menor que meio dia não move nada, e
|
|
293
|
+
arrastar o punho inicial para depois do fim **para** em `start === end` (1 dia)
|
|
294
|
+
em vez de inverter a barra — sem esse clamp a largura ficaria negativa e a barra
|
|
295
|
+
desapareceria no meio do gesto.
|
|
296
|
+
|
|
297
|
+
### 13. O `accessor` tem que casar com o `kind`
|
|
298
|
+
|
|
299
|
+
`number` precisa de número (ou string numérica); `date` precisa de `Date` ou ISO;
|
|
300
|
+
`boolean` aceita `true`/`1`/`"sim"`. Devolver a coisa errada **não é erro de tipo** —
|
|
301
|
+
o `accessor` é declarado largo de propósito — e o campo simplesmente não casa nada.
|
|
302
|
+
|
|
303
|
+
E `undefined` **exclui** a linha quando o filtro está ativo: filtrar por
|
|
304
|
+
"Responsável = Ana" e receber de volta as linhas sem responsável nenhum é o oposto
|
|
305
|
+
do pedido.
|
|
306
|
+
|
|
307
|
+
⚠️ Se você monta o valor de `date` na mão, **não** use `new Date("2026-09-30")`: ISO
|
|
308
|
+
date-only é parseado como **UTC** e volta um dia em fuso negativo (medido em UTC−3:
|
|
309
|
+
vira 29/09 21:00). O componente usa `parseDiaISO` internamente pelos dois lados —
|
|
310
|
+
predicado e chip — pra os dois nunca divergirem.
|
|
311
|
+
|
|
312
|
+
### 14. As duas visões respondem perguntas diferentes
|
|
313
|
+
|
|
314
|
+
| Visão | Responde | Título | `‹ ›` anda | Escala | Painel esquerdo |
|
|
315
|
+
|---|---|---|---|---|---|
|
|
316
|
+
| `timeline` | *o que depende do quê* | o intervalo ("31 ago – 2 nov 2026") | meia janela | ✅ | ✅ |
|
|
317
|
+
| `calendar` | *o que acontece no dia 12* | o **mês** ("outubro 2026") | **1 mês** | ⛔ | ⛔ |
|
|
318
|
+
| `list` | *o que vem a seguir* | o **mês** | **1 mês** | ⛔ | ⛔ |
|
|
319
|
+
|
|
320
|
+
O seletor é o **primeiro item da toolbar**, à esquerda, seguido de divisor —
|
|
321
|
+
igual à `DataTable`, onde o toggle Tabela/Lista/Kanban abre a toolbar. A razão é
|
|
322
|
+
hierárquica: a visão decide o que todo o resto da toolbar significa (o período
|
|
323
|
+
vira um mês no calendário, a escala desaparece), então ela não pode vir depois
|
|
324
|
+
do que governa.
|
|
325
|
+
|
|
326
|
+
**`list` é uma AGENDA, não uma tabela de tarefas.** Tabela com hierarquia e
|
|
327
|
+
filtro é o `DataTable` (view lista, `hierarchical`). O que faz esta visão
|
|
328
|
+
diferente é o agrupamento por **dia**: a mesma tarefa aparece em cada dia que
|
|
329
|
+
ocupa, com a posição no intervalo à direita ("dia 2 de 6"). Numa tarefa repetida
|
|
330
|
+
em 6 blocos, o intervalo seria idêntico nos seis e não diria nada novo — a
|
|
331
|
+
posição diz onde no trabalho aquele dia está.
|
|
332
|
+
|
|
333
|
+
⚠️ **Só os dias que TÊM tarefa aparecem.** Uma agenda que lista 60 dias pra
|
|
334
|
+
mostrar 20 tarefas obriga a rolar por 40 blocos vazios; o salto entre datas é o
|
|
335
|
+
próprio sinal de que não há nada no meio. (A grade de mês faz o oposto, e está
|
|
336
|
+
certa: lá o dia vazio *é* a informação.)
|
|
337
|
+
|
|
338
|
+
⚠️ **A agenda é recortada no MÊS, não na janela** — como a grade. Lendo a janela
|
|
339
|
+
inteira ela saía com **62 blocos de dia e 168 cartões** no exemplo: rolagem
|
|
340
|
+
infinita disfarçada de agenda. No mês são no máximo 31 blocos, e o `‹ ›` andando
|
|
341
|
+
um mês exato dá o gesto pra chegar nos outros. A janela existe pro EIXO, que
|
|
342
|
+
comprime 64 dias em pixels; agenda e grade não comprimem — cada dia custa uma
|
|
343
|
+
linha.
|
|
344
|
+
|
|
345
|
+
⚠️ Numa linha com **várias barras** (`lanePacking`), a sublinha do cartão é o
|
|
346
|
+
rótulo da BARRA, não o `sublabel` da linha. Sem isso a linha aparecia N vezes no
|
|
347
|
+
mesmo dia com título e sublinha idênticos, só o "dia N de M" diferente — N
|
|
348
|
+
cartões indistinguíveis.
|
|
349
|
+
|
|
350
|
+
A toolbar **muda com a visão**, não só o conteúdo. O título de `calendar` é o mês
|
|
351
|
+
porque a grade é de um mês — anunciar 64 dias sobre uma grade que mostra 31 seria
|
|
352
|
+
o título mentindo. O `‹ ›` anda um mês exato ali (meia janela poderia cair no
|
|
353
|
+
mesmo mês, e o botão não faria o que o título promete). E a escala **desaparece**:
|
|
354
|
+
"Dia/Semana/Mês/Trimestre" é a densidade do eixo horizontal, e na grade de mês não
|
|
355
|
+
há eixo pra adensar — controle que não muda nada é a mesma classe de defeito do
|
|
356
|
+
toggle de crítico sem vínculo.
|
|
357
|
+
|
|
358
|
+
⚠️ A escala **não é resetada** ao esconder: voltando pra timeline, o zoom que
|
|
359
|
+
estava selecionado continua. Zerar faria trocar de visão e voltar perder trabalho
|
|
360
|
+
do usuário.
|
|
361
|
+
|
|
362
|
+
### Recortar as visões — `views` ≠ `view`
|
|
363
|
+
|
|
364
|
+
`views` diz **o que existe**; `view` diz **o que está aberto**. Nem todo
|
|
365
|
+
cronograma quer as três: um painel de dependências não ganha nada com a agenda,
|
|
366
|
+
uma tela de acompanhamento mensal pode não querer o eixo.
|
|
367
|
+
|
|
368
|
+
```tsx
|
|
369
|
+
<Gantt rows={ROWS} views={["timeline"]} /> // só o cronograma
|
|
370
|
+
<Gantt rows={ROWS} views={["calendar", "list"]} /> // sem eixo
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
⚠️ **Com uma visão só, o seletor e o divisor não são renderizados.** Segmentado
|
|
374
|
+
de uma opção é um controle que nunca muda nada — a mesma regra do toggle de
|
|
375
|
+
crítico sem vínculo e do "+" da grade sem `onDayAdd`.
|
|
376
|
+
|
|
377
|
+
⚠️ **`views` vence `view`.** `view="list"` com `views={["timeline"]}` abre a
|
|
378
|
+
timeline: renderizar o que o consumidor excluiu seria pior que corrigir. Lista
|
|
379
|
+
vazia (`views={[]}`) é tratada como omitida.
|
|
380
|
+
|
|
381
|
+
### A toolbar tem dois slots livres
|
|
382
|
+
|
|
383
|
+
**É aqui que entra o botão de opções.** O componente não inventa "⋯", "exportar"
|
|
384
|
+
nem "imprimir" — ele abre espaço e você põe o que a sua tela precisa, do mesmo
|
|
385
|
+
jeito que a `TableToolbar` recebe `actions`.
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
[visão] │ [‹ Hoje ›] [período] … [busca] [toolbarActions] [crítico] [escala] [filtro] [primaryAction]
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
<Gantt
|
|
393
|
+
rows={ROWS}
|
|
394
|
+
toolbarActions={
|
|
395
|
+
<DropdownMenu>
|
|
396
|
+
<DropdownMenuTrigger asChild>
|
|
397
|
+
<Button variant="outline" color="secondary" size="icon-md"
|
|
398
|
+
aria-label="Mais opções"><MoreHorizontal /></Button>
|
|
399
|
+
</DropdownMenuTrigger>
|
|
400
|
+
<DropdownMenuContent align="end">…</DropdownMenuContent>
|
|
401
|
+
</DropdownMenu>
|
|
402
|
+
}
|
|
403
|
+
primaryAction={<Button iconLeft={<Plus />}>Nova tarefa</Button>}
|
|
404
|
+
/>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
`toolbarActions` entra **antes** da ação primária, de propósito: a ação primária
|
|
408
|
+
fecha a barra, e é o último lugar onde o olho para.
|
|
409
|
+
|
|
410
|
+
### Carregando ≠ vazio
|
|
411
|
+
|
|
412
|
+
⚠️ **Se você busca do servidor, passe `loading`.** Sem ele, `rows={[]}` durante o
|
|
413
|
+
fetch faz o componente afirmar *"Nenhuma tarefa neste período"* — uma frase que
|
|
414
|
+
ele não tem como saber que é verdade, e que faz o usuário desistir antes de o
|
|
415
|
+
dado chegar.
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
<Gantt rows={rows} loading={isFetching} />
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
`loading` **vence** `rows`: mesmo com linhas na tela ele mostra o esqueleto,
|
|
422
|
+
porque durante um refetch o que está renderizado pode estar velho. A **toolbar
|
|
423
|
+
continua** — período, busca e filtro não dependem dos dados, e apagá-la faria a
|
|
424
|
+
tela inteira piscar quando eles chegam.
|
|
425
|
+
|
|
426
|
+
O esqueleto imita a silhueta da visão atual (painel + barras na timeline, 6×7 na
|
|
427
|
+
grade, blocos na agenda), pra a chegada do dado não deslocar nada. `loadingState`
|
|
428
|
+
troca por um seu — mantenha a altura.
|
|
429
|
+
|
|
430
|
+
### O imperativo — `GanttRef`
|
|
431
|
+
|
|
432
|
+
Quatro métodos, pra quando a tela precisa mandar no cronograma sem passar por
|
|
433
|
+
prop: `goToDate(date)` · `goToToday()` · `expandAll()` · `collapseAll()`.
|
|
434
|
+
|
|
435
|
+
```tsx
|
|
436
|
+
const gantt = useRef<GanttRef>(null);
|
|
437
|
+
<Button onClick={() => gantt.current?.collapseAll()}>Recolher tudo</Button>
|
|
438
|
+
<Gantt ref={gantt} rows={ROWS} />
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Os três estados de UI seguem o MESMO par
|
|
442
|
+
|
|
443
|
+
`view`, `granularity` e `criticalPath` são estados que a UI do próprio
|
|
444
|
+
componente mexe — o seletor de visão, o dropdown de escala e o botão "Crítico".
|
|
445
|
+
Cada um aceita as duas formas:
|
|
446
|
+
|
|
447
|
+
| Você passa | Quem manda | O `on*Change` |
|
|
448
|
+
|---|---|---|
|
|
449
|
+
| nada | o componente | não existe pra chamar |
|
|
450
|
+
| só o valor | você — **o controle congela** | não existe pra chamar |
|
|
451
|
+
| valor + callback | você | avisa a intenção do usuário |
|
|
452
|
+
|
|
453
|
+
É a semântica normal de campo controlado no React, e a linha do meio é
|
|
454
|
+
deliberada — não é bug.
|
|
455
|
+
|
|
456
|
+
⛔ **`granularity` e `criticalPath` não tinham callback até a v0.59.** O
|
|
457
|
+
resultado era só a linha do meio, sem escapatória: passar `granularity="day"`
|
|
458
|
+
como valor inicial matava o dropdown de escala em silêncio — o clique mudava o
|
|
459
|
+
estado interno e a prop o mascarava de volta. Se você copiou um exemplo com
|
|
460
|
+
`granularity` fixo e a escala "não funciona", é isto: **remova a prop** (o
|
|
461
|
+
componente já começa em `day`) ou acrescente `onGranularityChange`.
|
|
462
|
+
|
|
463
|
+
**A barra é UM segmento contínuo** atravessando os dias que ocupa — não um chip
|
|
464
|
+
por dia. Uma tarefa de 10 a 15 é uma barra de 6 colunas, e se cruzar a virada de
|
|
465
|
+
semana vira dois segmentos com a ponta cortada marcada (mesmo vocabulário
|
|
466
|
+
`continuesBefore`/`continuesAfter` da timeline). Um chip por dia leria como seis
|
|
467
|
+
tarefas de um dia em vez de uma de seis.
|
|
468
|
+
|
|
469
|
+
`onDayAdd` liga o **"+" no hover da célula**. Sem o handler o botão não é
|
|
470
|
+
renderizado — um "+" que não adiciona nada é pior que a ausência dele.
|
|
471
|
+
|
|
472
|
+
⚠️ Ele significa **"o usuário pediu pra adicionar no dia X"**, não "adicione
|
|
473
|
+
isto". Abra seu drawer/modal com os campos que a SUA tela precisa (nome, cor,
|
|
474
|
+
responsável, duração) e crie a linha no salvar — mesma divisão do `onBarClick`,
|
|
475
|
+
que devolve o payload e deixa a ficha pra você. O `GanttFullPreview` mostra o
|
|
476
|
+
drawer completo com `FormFieldInput`/`FormFieldSelect`.
|
|
477
|
+
|
|
478
|
+
⛔ Não ofereça "escolha uma cor" por NOME DE COR no seu formulário. A cor da barra
|
|
479
|
+
diz CATEGORIA (qual frente), e um seletor de "verde/roxo" convida o usuário a usar
|
|
480
|
+
cor como STATUS — que é exatamente a assumption que este componente declara.
|
|
481
|
+
Rotule as opções pela frente ("Design", "Engenharia") e mapeie pra `colorKey` por
|
|
482
|
+
dentro.
|
|
483
|
+
|
|
484
|
+
⚠️ **Linha `summary` não entra na grade de mês.** O intervalo dela é a união dos
|
|
485
|
+
filhos, então ela pintaria exatamente as células que os filhos já pintam — e ali a
|
|
486
|
+
hierarquia não é visível pra dar sentido ao agregado.
|
|
487
|
+
|
|
488
|
+
⚠️ **O corte do `+N mais` é MEDIDO, não fixo.** A célula tem altura mínima de 88px
|
|
489
|
+
(cabeçalho + 3 chips) e a grade rola quando não cabe; um `ResizeObserver` recalcula
|
|
490
|
+
quantos chips entram quando a altura muda. Com corte fixo, uma viewport curta
|
|
491
|
+
recortava os chips e o próprio `+N mais` — o usuário via 2 tarefas e nenhuma pista
|
|
492
|
+
de que havia 4.
|
|
493
|
+
|
|
494
|
+
### 15. Em telas estreitas, a TOOLBAR se adapta — a MATRIZ não
|
|
495
|
+
|
|
496
|
+
São dois problemas diferentes, e só um tem solução dentro do componente.
|
|
497
|
+
|
|
498
|
+
**A toolbar resolve sozinha, com a regra da `TableToolbar`.** Abaixo de `md`
|
|
499
|
+
(768px) saem da barra o **seletor de visão**, o **`‹ Hoje ›`**, a **escala** e o
|
|
500
|
+
**caminho crítico**; eles reaparecem num menu (ícone de sliders) ao lado do
|
|
501
|
+
título, que abre como bottom-sheet. Ficam na barra: **título do período**,
|
|
502
|
+
**busca**, **filtro** e a **ação primária**.
|
|
503
|
+
|
|
504
|
+
Medido a 375px: antes, a busca era espremida a **51px** (o ícone e nada mais) e o
|
|
505
|
+
título saía cortado no meio; depois, a busca tem **153px** e o título cabe
|
|
506
|
+
inteiro. O título é a única coisa que o Gantt preserva e a tabela não tem — sem
|
|
507
|
+
ele o usuário não sabe que período está vendo.
|
|
508
|
+
|
|
509
|
+
⚠️ **A `primaryAction` é sua.** O componente não pode encolhê-la (é um
|
|
510
|
+
`ReactNode` que você passa). Em mobile ela come ~126px com rótulo — passe um
|
|
511
|
+
botão **icon-only** abaixo de `md` se a barra ficar apertada.
|
|
512
|
+
|
|
513
|
+
**A matriz não cabe, e o componente não finge que cabe.** Grade + eixo em 375px
|
|
514
|
+
não é problema de layout, é de densidade: um Gantt legível precisa de ~900px.
|
|
515
|
+
Use `granularity="week"` ou mais, reduza `gridWidth`, ou — melhor — mande
|
|
516
|
+
`view="calendar"` / `view="list"` no mobile, que são justamente as visões sem
|
|
517
|
+
eixo horizontal.
|
|
518
|
+
|
|
519
|
+
## ⚠️ O que ainda NÃO existe
|
|
520
|
+
|
|
521
|
+
Declarado pra não virar descoberta:
|
|
522
|
+
|
|
523
|
+
| | Estado |
|
|
524
|
+
|---|---|
|
|
525
|
+
| setas de vínculo na visão `calendar` | **não existem, e é decisão** — numa grade de mês uma seta do dia 3 ao 19 atravessaria 3 semanas passando por cima de 16 células alheias. O grafo segue vivo (conflito marcado no chip, `onLinkViolations` emitindo); sai só o desenho |
|
|
526
|
+
| gesto na visão `calendar` | só clique. Arrastar num calendário move por dia, não por pixel — é outro gesto, e misturá-lo com o da timeline daria duas semânticas pro mesmo arraste |
|
|
527
|
+
| criar vínculo `FF` / `SF` por gesto | possível, mas exige soltar na **metade direita** da barra de destino. Descoberta só pela doc — não há dica visual da metade |
|
|
528
|
+
|
|
529
|
+
O que está completo: as duas visões de dado (tarefa e portfólio), hierarquia com
|
|
530
|
+
collapse e **conectores de árvore**, `summary` derivado, marcos, progresso, os
|
|
531
|
+
**4 tipos de vínculo** com `lag`, detecção de conflito, caminho crítico, busca,
|
|
532
|
+
zoom em 4 escalas, divisor arrastável, **filtro nos 6 tipos** com painel lateral
|
|
533
|
+
e chips de aplicado, seleção de linha e de coluna, virada de mês no eixo e os
|
|
534
|
+
**4 gestos** (mover, redimensionar, criar e remover vínculo) com snap de dia, e as
|
|
535
|
+
**três visões** (`timeline`, `calendar`, `list`) com o seletor segmentado abrindo
|
|
536
|
+
a toolbar.
|
|
537
|
+
|
|
538
|
+
## Núcleo puro exportado
|
|
539
|
+
|
|
540
|
+
Útil fora do render — validar cronograma no servidor, calcular crítico num job:
|
|
541
|
+
|
|
542
|
+
```tsx
|
|
543
|
+
import { checkAllLinks, computeCriticalPath, topoSort } from "@/components/ui/Gantt";
|
|
544
|
+
import { buildTimeAxis, clipToWindow, packLanes } from "@/components/ui/Gantt";
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**146 testes** cobrem as bordas: virada de mês, barra que cruza a janela, lane
|
|
548
|
+
packing com sobreposição parcial, ciclo em `parent` (que devolvia lista VAZIA até
|
|
549
|
+
o teste existir), auto-vínculo, as 4 restrições nas pontas certas, o índice
|
|
550
|
+
`ancestorHasNext[i+1]` do conector (L-045) e o parse de `YYYY-MM-DD` como
|
|
551
|
+
meia-noite local (com `new Date` era UTC e o filtro de data voltava um dia), e o
|
|
552
|
+
clamp do resize que impedia a barra de inverter e desaparecer.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Header — USAGE
|
|
2
|
+
|
|
3
|
+
Barra superior fixa (60px) com breadcrumb à esquerda + search/theme/notifications/messages/user à direita.
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
- Topo de qualquer página dentro de `<AppShell>`
|
|
7
|
+
- Standalone (raro): pages que não usam AppShell mas precisam de chrome consistente
|
|
8
|
+
|
|
9
|
+
## Import
|
|
10
|
+
```tsx
|
|
11
|
+
import { Header } from "@/components/ui/Header";
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Props essenciais
|
|
15
|
+
| Prop | Tipo | Função |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `breadcrumb` | `HeaderBreadcrumbItem[]` | Array de items (último = página atual, nunca link). Ver a forma completa abaixo |
|
|
18
|
+
| `onCollapseMenu` | () => void | Botão de collapse do MenuSidebar (omitido = botão escondido) |
|
|
19
|
+
| `menuCollapsed` | boolean | Controla o ícone (PanelLeftClose vs PanelLeftOpen) |
|
|
20
|
+
| `showSearch` | boolean (default `true`) | Mostra o fake-input de busca; `false` desliga |
|
|
21
|
+
| `searchPlaceholder` | string | Texto do fake-input de busca |
|
|
22
|
+
| `commandGroups` | `HeaderCommandGroup[]` | Comandos do Command palette interno — sem isso o palette abre vazio |
|
|
23
|
+
| `theme` | string | Tema ativo |
|
|
24
|
+
| `themeOptions` | HeaderThemeOption[] | Lista de temas pro dropdown |
|
|
25
|
+
| `onThemeChange` | (id: string) => void | Callback |
|
|
26
|
+
| `notifications` | { items, onMarkAllRead, onViewAll } | Popover sino (vira bottom-sheet no mobile via `mobileSheet`) |
|
|
27
|
+
| `messages` | { items, onNewMessage, onExpand, onViewAll } | Popover chat (vira bottom-sheet no mobile via `mobileSheet`) |
|
|
28
|
+
| `rightSlot` | ReactNode | Slot livre à direita (botões custom antes dos ícones default) |
|
|
29
|
+
|
|
30
|
+
### `HeaderBreadcrumbItem` — a forma completa
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
{
|
|
34
|
+
label: string;
|
|
35
|
+
href?: string; // vira link; o ÚLTIMO item nunca é link
|
|
36
|
+
onClick?: (e) => void;
|
|
37
|
+
|
|
38
|
+
// Vira SELETOR de registro. Precisa dos TRÊS juntos; faltando um, o item
|
|
39
|
+
// renderiza como texto — gatilho que abre lista vazia é pior que texto.
|
|
40
|
+
switcher?: BreadcrumbSwitcherOption[];
|
|
41
|
+
value?: string;
|
|
42
|
+
onValueChange?: (v: string) => void;
|
|
43
|
+
switcherTitle?: ReactNode;
|
|
44
|
+
switcherSearchPlaceholder?: string;
|
|
45
|
+
switcherFooter?: ReactNode;
|
|
46
|
+
|
|
47
|
+
// Conteúdo livre DEPOIS do rótulo, no mesmo item.
|
|
48
|
+
trailing?: ReactNode;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**`trailing`** aceita qualquer nó — o caso de origem foi um chip de status ao lado
|
|
53
|
+
do nome do registro aberto (`Clientes / Maria Silva [Ativo]`), mas o slot não sabe
|
|
54
|
+
disso. É a única forma de pôr algo ali neste modo: quem monta o `<li>` é o
|
|
55
|
+
componente, então não há onde escrever um irmão na mão.
|
|
56
|
+
|
|
57
|
+
⚠️ Ele é **irmão** do gatilho do seletor, não filho — é o que permite conteúdo
|
|
58
|
+
interativo (`<button>` dentro de `<button>` é HTML inválido). Consequência: clicar
|
|
59
|
+
no `trailing` **não** abre a lista do seletor.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<Header
|
|
63
|
+
breadcrumb={[
|
|
64
|
+
{ label: "Clientes", href: "/clientes" },
|
|
65
|
+
{
|
|
66
|
+
label: cliente.nome,
|
|
67
|
+
switcher: CLIENTES, value: id, onValueChange: abrirCliente,
|
|
68
|
+
trailing: <Chip size="sm" variant="soft" color={cliente.statusCor}>{cliente.status}</Chip>,
|
|
69
|
+
},
|
|
70
|
+
]}
|
|
71
|
+
/>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Exemplo mínimo
|
|
75
|
+
```tsx
|
|
76
|
+
<Header
|
|
77
|
+
breadcrumb={[{ label: "Clientes" }, { label: "Maria Silva" }]}
|
|
78
|
+
onCollapseMenu={() => setCollapsed((c) => !c)}
|
|
79
|
+
menuCollapsed={collapsed}
|
|
80
|
+
searchPlaceholder="Buscar..."
|
|
81
|
+
commandGroups={[
|
|
82
|
+
{
|
|
83
|
+
heading: "Ações",
|
|
84
|
+
items: [{ label: "Novo cliente", onSelect: () => abrirDrawer() }],
|
|
85
|
+
},
|
|
86
|
+
]}
|
|
87
|
+
theme={theme}
|
|
88
|
+
onThemeChange={setTheme}
|
|
89
|
+
themeOptions={APP_SHELL_THEME_OPTIONS}
|
|
90
|
+
notifications={{ items: notifs, onMarkAllRead, onViewAll }}
|
|
91
|
+
/>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Cuidados / Gotchas
|
|
95
|
+
- Position é responsabilidade do consumer/template (Header só define altura + layout)
|
|
96
|
+
- Breadcrumb com 1 único item renderiza automaticamente como título standalone (15px); 2+ items viram cadeia (13px). Último item nunca é link
|
|
97
|
+
- Search é fake-input que abre o Command palette interno (⌘K / Ctrl+K) — popular via `commandGroups`, senão o palette abre vazio
|
|
98
|
+
- Badge dot no icon button: `kind="brand"` (mensagens) vs `kind="danger"` (alertas)
|