@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.
Files changed (65) hide show
  1. package/dist-lib/ai/componentes/AlertModal.md +47 -0
  2. package/dist-lib/ai/componentes/AppShell.md +117 -0
  3. package/dist-lib/ai/componentes/Breadcrumb.md +187 -0
  4. package/dist-lib/ai/componentes/Button.md +116 -0
  5. package/dist-lib/ai/componentes/ButtonGroup.md +162 -0
  6. package/dist-lib/ai/componentes/CardCheckbox.md +99 -0
  7. package/dist-lib/ai/componentes/CardOption.md +133 -0
  8. package/dist-lib/ai/componentes/Chart.md +93 -0
  9. package/dist-lib/ai/componentes/Chip.md +68 -0
  10. package/dist-lib/ai/componentes/ChoroplethMap.md +118 -0
  11. package/dist-lib/ai/componentes/ColorPicker.md +70 -0
  12. package/dist-lib/ai/componentes/Combobox.md +51 -0
  13. package/dist-lib/ai/componentes/ConversationListItem.md +90 -0
  14. package/dist-lib/ai/componentes/DataList.md +111 -0
  15. package/dist-lib/ai/componentes/DataTable.md +867 -0
  16. package/dist-lib/ai/componentes/DatePicker.md +84 -0
  17. package/dist-lib/ai/componentes/DateSeparatorChip.md +59 -0
  18. package/dist-lib/ai/componentes/EmptyState.md +72 -0
  19. package/dist-lib/ai/componentes/FileUploadField.md +95 -0
  20. package/dist-lib/ai/componentes/FloatingPanel.md +118 -0
  21. package/dist-lib/ai/componentes/FooterTable.md +62 -0
  22. package/dist-lib/ai/componentes/FormField.md +110 -0
  23. package/dist-lib/ai/componentes/Gantt.md +552 -0
  24. package/dist-lib/ai/componentes/Header.md +98 -0
  25. package/dist-lib/ai/componentes/Icon.md +65 -0
  26. package/dist-lib/ai/componentes/Kanban.md +343 -0
  27. package/dist-lib/ai/componentes/Kpi.md +103 -0
  28. package/dist-lib/ai/componentes/List.md +61 -0
  29. package/dist-lib/ai/componentes/MarkdownText.md +59 -0
  30. package/dist-lib/ai/componentes/MenuSidebar.md +128 -0
  31. package/dist-lib/ai/componentes/MessageAck.md +53 -0
  32. package/dist-lib/ai/componentes/MessageBubble.md +115 -0
  33. package/dist-lib/ai/componentes/MessageComposer.md +80 -0
  34. package/dist-lib/ai/componentes/MessageVariablesPicker.md +104 -0
  35. package/dist-lib/ai/componentes/Modal.md +88 -0
  36. package/dist-lib/ai/componentes/MonthYearPicker.md +49 -0
  37. package/dist-lib/ai/componentes/PageHeader.md +129 -0
  38. package/dist-lib/ai/componentes/Panel.md +84 -0
  39. package/dist-lib/ai/componentes/Scheduler.md +421 -0
  40. package/dist-lib/ai/componentes/ScreenLoader.md +60 -0
  41. package/dist-lib/ai/componentes/SingleMenuSidebar.md +171 -0
  42. package/dist-lib/ai/componentes/Spinner.md +52 -0
  43. package/dist-lib/ai/componentes/Table.md +192 -0
  44. package/dist-lib/ai/componentes/TableToolbar.md +87 -0
  45. package/dist-lib/ai/componentes/TabsNavigation.md +152 -0
  46. package/dist-lib/ai/componentes/Toast.md +49 -0
  47. package/dist-lib/ai/componentes/_primitivos.md +74 -0
  48. package/dist-lib/ai/componentes/avatar-ig.md +181 -0
  49. package/dist-lib/ai/componentes/indice.json +49 -0
  50. package/dist-lib/ai/exemplos/dashboard/dashboard-brazil-map.ts +33 -0
  51. package/dist-lib/ai/exemplos/dashboard/dashboard-screen.tsx +1110 -0
  52. package/dist-lib/ai/exemplos/dashboard/index.ts +1 -0
  53. package/dist-lib/ai/global/componentes.md +217 -0
  54. package/dist-lib/ai/global/composicao.md +182 -0
  55. package/dist-lib/ai/indice.json +177 -0
  56. package/dist-lib/ai/lint/ds-lint-patterns.mjs +115 -0
  57. package/dist-lib/ai/manifest.json +16 -0
  58. package/dist-lib/ai/regras/design.md +88 -0
  59. package/dist-lib/ai/regras/temas.md +192 -0
  60. package/dist-lib/ai/regras-por-componente.json +103 -0
  61. package/dist-lib/ai/roteiros/dashboard/blueprint.md +47 -0
  62. package/dist-lib/ai/roteiros/dashboard/entrevista.md +62 -0
  63. package/dist-lib/ai/roteiros/dashboard/geracao.md +88 -0
  64. package/dist-lib/ai/roteiros/dashboard/roteiro.md +88 -0
  65. package/package.json +4 -1
@@ -0,0 +1,60 @@
1
+ # ScreenLoader
2
+
3
+ <!-- ds:regras
4
+ - omita `size`: `md` é o calibrado pro slot do AppShell; `lg` não é "pra dar destaque"
5
+ - omita `skeletonLayout`: `page` serve pra qualquer tela — só mude se ela TEM KPIs no topo
6
+ - o pai precisa ter altura, senão a variante spinner colapsa no topo
7
+ - loading inline (botão, célula) → `Spinner` direto; overlay é composição do consumidor
8
+ -->
9
+
10
+ **Categoria:** iGreen (tv()). Estado de **carregamento de página/área** — preenche o container pai e mostra um indicador enquanto o conteúdo processa. Irmão do `EmptyState` (mesma família de "estado de área").
11
+
12
+ ## Quando usar
13
+
14
+ - Conteúdo de uma página inteira carregando dentro do slot de conteúdo do AppShell.
15
+ - Área grande (card, section, painel) aguardando primeira carga de dados.
16
+ - **Não** é pra loading inline (botão, célula) — aí use `Spinner` direto.
17
+ - **Não** é overlay sobre conteúdo já renderizado — cobre o slot vazio; sobrepor é composição do consumidor.
18
+
19
+ ## Props essenciais
20
+
21
+ | Prop | Tipo | Default | Descrição |
22
+ |------|------|---------|-----------|
23
+ | `variant` | `"spinner" \| "skeleton"` | `"spinner"` | `spinner` = Spinner centrado + título + descrição. `skeleton` = silhueta genérica de página, sem prever o layout final. |
24
+ | `skeletonLayout` | `"page" \| "dashboard" \| "kpis"` | `"page"` | Blocos da silhueta (só `skeleton`): `page` = header + conteúdo · `dashboard` = header + linha de 4 KPIs + conteúdo · `kpis` = KPIs + conteúdo, sem header. |
25
+ | `title` | `string` | `"Carregando…"` | Visível no `spinner`; sr-only no `skeleton` (leitores anunciam via `role="status"`). |
26
+ | `description` | `string` | — | Linha auxiliar sob o título (só `spinner`). |
27
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Escala spinner + tipografia do título. Sem efeito no `skeleton`. **`md` é o padrão de uso** — não passe `size` a menos que a área destoe (ver Gotchas). |
28
+ | `color` | cores do Spinner | `"brand"` | Repassada ao Spinner (só `spinner`). |
29
+ | `className` | `string` | — | Overrides no root (aceita `ref` de `HTMLDivElement`). |
30
+
31
+ ## Exemplo mínimo
32
+
33
+ ```tsx
34
+ import { ScreenLoader } from "@snksergio/design-system";
35
+
36
+ // dentro do slot de conteúdo (o pai precisa ter altura)
37
+ {isLoading ? (
38
+ <ScreenLoader title="Carregando clientes" description="Buscando os dados mais recentes…" />
39
+ ) : (
40
+ <ClientesPage />
41
+ )}
42
+
43
+ // variação skeleton — silhueta genérica em vez de spinner
44
+ {isLoading ? <ScreenLoader variant="skeleton" /> : <ClientesPage />}
45
+
46
+ // tela de painel: skeleton com linha de KPIs (com ou sem header)
47
+ {isLoading ? <ScreenLoader variant="skeleton" skeletonLayout="dashboard" /> : <Dashboard />}
48
+ {isLoading ? <ScreenLoader variant="skeleton" skeletonLayout="kpis" /> : <KpiSection />}
49
+ ```
50
+
51
+ ## Gotchas
52
+
53
+ - **Padrão de uso: omita `size` (= `md`).** É o calibrado pro slot de conteúdo do AppShell. `sm` só pra área comprovadamente pequena (card baixo, painel lateral); `lg` só pra tela cheia vazia sem chrome. Não escolha `lg` "pra dar destaque".
54
+ - **Padrão do skeleton: omita `skeletonLayout` (= `page`, header + conteúdo).** É a silhueta genérica que serve pra qualquer tela — só saia dela quando a tela alvo COMPROVADAMENTE tem KPIs no topo: `dashboard` (com header de página) ou `kpis` (quando o header já está renderizado fora da área que carrega). Não invente combinação além dessas três: layout mais específico que isso = compor `Skeleton` na mão.
55
+
56
+ - **O pai precisa ter altura.** O componente preenche o container (`h-full flex-1`) — num pai sem altura definida, a variante `spinner` colapsa no topo (o `skeleton` tem `min-h` próprio de fallback). No AppShell o slot de conteúdo já tem altura.
57
+ - **Nunca `position: fixed`** — cobrir o viewport inteiro (splash, auth guard) é composição do consumidor, de propósito.
58
+ - **Skeleton genérico de propósito**: quando o layout final é conhecido (tabela, lista de cards), componha `<Skeleton>` na mão desenhando a silhueta real — o `DataTable`/`DataList` já trazem os próprios skeletons; não empilhe este por cima.
59
+ - **A11y sem duplicação**: o root tem `role="status"` + `aria-live="polite"`; o Spinner interno vai `aria-hidden`. Não embrulhe em outro `role="status"`.
60
+ - A rotação do Spinner para sob `prefers-reduced-motion` (`motion-reduce:animate-none`); o pulse do Skeleton (só opacidade, sem deslocamento) **não** é gated — é o comportamento do `shadcn/skeleton.tsx`.
@@ -0,0 +1,171 @@
1
+ # SingleMenuSidebar
2
+
3
+ <!-- ds:regras
4
+ - NÃO passe `logo`: o default é a marca iGreen. Só com marca própria pedida explicitamente
5
+ - `title` = nome do projeto (vai à direita da logo) — pergunte, não invente
6
+ - é a escolha quando NÃO há divisão em áreas: `showSearch={false}`, sem `module`/`modules`
7
+ -->
8
+
9
+ **O que é** — Sidebar de navegação de **nível único**: categoria → sub-itens em
10
+ accordion (1 aberto por vez). Categoria sem `items` é link simples.
11
+ **Categoria**: Templates / App-level (mesma família de `MenuSidebar`, `Header`, `AppShell`).
12
+
13
+ **Quando usar** — App com navegação plana, sem múltiplos contextos/rail. É a
14
+ alternativa **enxuta** ao `MenuSidebar` (que tem rail + painel + sections +
15
+ badges). Sem variantes — escolha o componente pela necessidade, não por props.
16
+ Precisa de rail + contextos + bookmarks/chats? → use `MenuSidebar`.
17
+
18
+ **Qual das duas** — o critério é *o sistema tem áreas grandes e separadas, cada uma com o
19
+ menu dela?* Sim → `MenuSidebar` (as áreas ficam todas visíveis no rail). Não → esta, na
20
+ variação **"Sem módulo / sem busca"** (omita `module`/`modules`, passe `showSearch={false}`).
21
+ Ela **também** aceita módulos, mas aí eles ficam atrás de um botão acima da busca — por isso
22
+ não é a recomendação quando a divisão existe.
23
+
24
+ **A logo é a da iGreen por default** — `logo` é opcional desde a 0.46.0 e cai em
25
+ `SidebarBrandMark`. Só passe quando o app tem marca própria; `title` é o **nome do projeto**,
26
+ exibido à direita dela.
27
+
28
+ > ⚠️ Até a 0.46.0 `logo` era **obrigatória e sem fallback**, enquanto o `brand` do `MenuSidebar`
29
+ > sempre foi opcional. Trocar pra esta sidebar portanto *forçava* quem montava a inventar uma
30
+ > logo — e foi assim que a marca da iGreen sumiu num app de consumidor (2026-08-22).
31
+
32
+ ## Props essenciais
33
+
34
+ | Prop | Tipo | Default | Obrigatório |
35
+ | ------------------------------- | ----------------------------- | ------- | ----------- |
36
+ | `logo` | `ReactNode` | **marca iGreen** | — (omita p/ ficar com a marca) |
37
+ | `title` | `string` | — | ✅ |
38
+ | `user` | `SingleMenuUser` | — | ✅ |
39
+ | `categories` | `SingleMenuCategory[]` | — | opcional se usar `modules` |
40
+ | `modules` | `SingleMenuModuleConfig[]` | — | |
41
+ | `activeModuleId` / `defaultModuleId` / `onModuleChange` | multi-módulo (controlado / inicial / callback) | — | |
42
+ | `module` | `SingleMenuModule` | — | **ignorado se `modules`** |
43
+ | `showSearch` | `boolean` | `true` | |
44
+ | `searchCommand` | `ReactNode` | — | |
45
+ | `searchPlaceholder` | `string` | — | |
46
+ | `activeItemId` | `string` | — | |
47
+ | `onItemClick` | `(id: string) => void` | — | |
48
+ | `defaultExpanded` | `boolean` | `true` | |
49
+ | `expanded` / `onExpandedChange` | toggle controlado | — | |
50
+ | `showToggleIndicator` | `boolean` | `false` | |
51
+
52
+ ⚠️ **A busca NÃO é input controlado.** `searchValue`/`onSearchChange`/`searchRef` **não
53
+ existem** nesta API (estavam documentados aqui e nunca foram props do componente). A busca
54
+ abre um `CommandDialog`: você passa o **conteúdo** dele em `searchCommand` e, se quiser, o
55
+ texto do placeholder em `searchPlaceholder`. As props `value`/`onChange`/`inputRef` existem
56
+ em `SingleMenuSearchProps`, que é subcomponente interno.
57
+
58
+ ### Multi-módulo
59
+
60
+ `modules` é a API pra sidebar que troca de contexto: cada `SingleMenuModuleConfig` traz suas
61
+ próprias `categories`, e trocar de módulo **sobrepõe** as categorias exibidas. Quando
62
+ `modules` é passado, `module` (singular) é ignorado e `categories` no nível raiz vira
63
+ opcional.
64
+
65
+ ## Data model
66
+
67
+ ```ts
68
+ SingleMenuCategory = { id, icon, label, href?, items?, active? }
69
+ SingleMenuSubItem = { id, label, href? }
70
+ SingleMenuModule = { icon, title, subtitle, options?, onModuleChange? }
71
+ SingleMenuUser = { name, email, avatar?, actions?, onAction? }
72
+ SingleMenuUserAction= { id, label, icon?, variant?: "default" | "destructive" }
73
+ ```
74
+
75
+ ## Navegação — `href` e `renderLink`
76
+
77
+ Item **com `href`** vira `<a href>`: ctrl/cmd+clique abre em nova aba, "copiar endereço do
78
+ link" funciona, e o leitor de tela anuncia **link** (com `aria-current="page"` no ativo).
79
+ Sem `href`, continua `<button>`. Vale pro sub-item e pra **categoria-folha** (sem `items`).
80
+
81
+ Pra integrar o router do consumidor, use **`renderLink`** — render-prop, não componente:
82
+
83
+ ```tsx
84
+ <SingleMenuSidebar
85
+ categories={categories}
86
+ onItemClick={(id) => setAtivo(id)}
87
+ renderLink={({ href, ...rest }) => <Link to={href} {...rest} />}
88
+ />
89
+ ```
90
+
91
+ ⚠️ **Sem `renderLink`**, o clique cancela a navegação nativa quando há handler — **exceto**
92
+ em clique modificado, `target="_blank"`, href externo (`https:`, `mailto:`…) e **href de
93
+ hash** (`#/rota`). A regra e o porquê de cada exceção estão em `@/utils/nav-link`, que é a
94
+ MESMA lógica do `MenuSidebar` (util compartilhada, não cópia).
95
+
96
+ > ⚠️ Até 2026-08-18 o `href` acima **não fazia nada**: o tipo o aceitava, este USAGE o
97
+ > documentava, e o componente renderizava `<button>` sempre. Se você escreveu código
98
+ > contando com navegação por `href` aqui, ele nunca navegou — agora navega.
99
+
100
+ ## Exemplo mínimo
101
+
102
+ ```tsx
103
+ import { SingleMenuSidebar } from "@/components/ui/SingleMenuSidebar";
104
+
105
+ <SingleMenuSidebar
106
+ logo={<Logo />}
107
+ title="Sólis iGreen"
108
+ module={{
109
+ icon: <Zap />,
110
+ title: "Créditos",
111
+ subtitle: "MÓDULO ATIVO",
112
+ options,
113
+ }}
114
+ categories={[
115
+ { id: "dashboard", icon: <LayoutGrid />, label: "Dashboard", active: true },
116
+ {
117
+ id: "instalacoes",
118
+ icon: <Zap />,
119
+ label: "Instalações",
120
+ items: [
121
+ { id: "contratos", label: "Contratos" },
122
+ { id: "vistorias", label: "Vistorias" },
123
+ ],
124
+ },
125
+ ]}
126
+ user={{ name: "Sérgio", email: "sergio@igreen.com.br", actions }}
127
+ activeItemId={activeItemId}
128
+ onItemClick={setActiveItemId}
129
+ />;
130
+ ```
131
+
132
+ ## Comportamentos
133
+
134
+ - **Accordion** — apenas 1 categoria aberta por vez. Definir `activeItemId` abre
135
+ automaticamente a categoria que contém o item.
136
+ - **Seleção única** — sempre 1 item marcado: folha ativa OU pai aberto (abrir um
137
+ pai o marca e suprime a folha) OU pai que contém o sub-item ativo. Clicar numa
138
+ folha fecha o pai aberto e assume a marca.
139
+ - **Toggle** — botão no header trava/destrava o estado expandido. Após recolher
140
+ manualmente, o hover-expand fica suprimido ~500ms (não "pisca" com o mouse por cima).
141
+ - **Hover-to-expand** — recolhida, o hover sobre a sidebar a expande
142
+ temporariamente; sai o mouse, recolhe (~200ms). Categorias mostram tooltip.
143
+ ⚠️ **A espiada FLUTUA sobre o conteúdo — não empurra** (v0.43.0+). No desktop o
144
+ painel vira `absolute z-40` e o `<aside>` continua ocupando só a largura do rail,
145
+ então o que está ao lado não se mexe. **Só o clique** (travar aberta) ocupa espaço
146
+ no fluxo e empurra — é decisão de layout do usuário, e quem está com o menu
147
+ recolhido escolheu maximizar a área de conteúdo.
148
+ Antes disso a espiada animava a largura **dentro** do fluxo: numa tela com
149
+ `DataTable` isso disparava 5 recálculos completos de largura de coluna por gesto,
150
+ ~100ms de travada e **CLS 0,117**. É o mesmo desenho que o `MenuSidebar` sempre
151
+ teve (painel flutuante), agora aqui. Se você depende do empurrão no hover, use
152
+ `expanded` controlado e trave aberta.
153
+ - **Controlado/não-controlado** — `expanded` + `onExpandedChange` (controlado) ou
154
+ `defaultExpanded` (não-controlado).
155
+ - **Responsivo (mobile)** — abaixo de `md` (768px) a sidebar ocupa **100% da
156
+ largura** (pronta pra drawer); no desktop mantém a largura fixa (280px/80px). A
157
+ sidebar é **dumb**: exibir/ocultar no mobile é responsabilidade do consumidor
158
+ (um toggle/drawer controlado pelo seu app — veja o exemplo "Responsivo (mobile)").
159
+
160
+ ## Gotchas
161
+
162
+ - **Sem variantes (por design).** Não há `variant`/`size`. Mudança visual = editar
163
+ `single-menu-sidebar.styles.ts`. Outra forma de navegação = outro componente.
164
+ - **Dá altura ao container.** O `<aside>` é `h-full` — o pai precisa ter altura
165
+ (ex.: `h-[680px]` ou `flex-1 min-h-0` num pai `h-full`).
166
+ - **`logo` e `avatar` são ReactNode** — você controla o tamanho; o slot do logo só
167
+ faz `shrink-0`. Passe um elemento já dimensionado (ex.: caixa `size-form-lg`).
168
+ - **Cores 100% via tokens DS.** Estado marcado = `fg-brand` + `bg-sidebar-accent`;
169
+ rodapé/hover = `bg-sidebar-accent`. Não usa palette própria `sidebar-*`.
170
+ - **`<TooltipProvider>` embutido** — o componente já envolve a árvore; não precisa
171
+ de provider externo só pra ele.
@@ -0,0 +1,52 @@
1
+ # Spinner
2
+
3
+ **Categoria:** iGreen (tv()). Indicador de **loading** (SVG que gira).
4
+
5
+ ## Quando usar
6
+
7
+ - Estado de carregamento inline: dentro de botões (`Salvando…`), ao lado de um label, em placeholders de área que ainda vai popular.
8
+ - Para um bloco de página inteiro carregando, componha com o seu próprio layout (ex.: centralizar `<Spinner size="lg" />`).
9
+
10
+ Não é barra de progresso determinística — para isso use `Progress`.
11
+
12
+ ## Props essenciais
13
+
14
+ | Prop | Tipo | Default | Descrição |
15
+ |------|------|---------|-----------|
16
+ | `size` | `"sm" \| "md" \| "lg" \| "xl" \| "2xl"` | `"md"` | Tamanho via `size-icon-*` (16/20/24/32/40px). `xl`/`2xl` são pra loading de área/página (é o que o `ScreenLoader` usa). |
17
+ | `color` | `"current" \| "default" \| "muted" \| "brand" \| "on-brand"` | `"muted"` | Cor via `text-fg-*`. `current` herda a cor do texto do pai. |
18
+ | `label` | `string` | `"Carregando"` | Rótulo acessível (`role="status"`). |
19
+ | `className` | `string` | — | Overrides (o SVG é o próprio nó — aceita `ref` para `SVGSVGElement`). |
20
+
21
+ Aceita qualquer prop de `<svg>` (menos `color`, sobrescrita pela variante).
22
+
23
+ ## Variantes
24
+
25
+ | Variante | Valores |
26
+ |----------|---------|
27
+ | `size` | `sm` · `md` · `lg` · `xl` · `2xl` |
28
+ | `color` | `current` · `default` · `muted` · `brand` · `on-brand` |
29
+
30
+ ## Exemplo mínimo
31
+
32
+ ```tsx
33
+ import { Spinner } from "@snksergio/design-system";
34
+
35
+ // standalone, neutro
36
+ <Spinner />
37
+
38
+ // dentro de um botão primário (herda o branco do texto do botão)
39
+ <Button disabled>
40
+ <Spinner size="sm" color="current" aria-hidden />
41
+ Salvando…
42
+ </Button>
43
+
44
+ // destaque de marca, maior
45
+ <Spinner size="lg" color="brand" label="Carregando ranking" />
46
+ ```
47
+
48
+ ## Gotchas
49
+
50
+ - **Respeita `prefers-reduced-motion`**: quando o usuário pede menos movimento, a rotação para (`motion-reduce:animate-none`) — o spinner fica estático mas ainda comunica "carregando" via `role="status"`.
51
+ - **Decorativo dentro de botões**: passe `aria-hidden` — aí o `label`/`role="status"` são omitidos (o texto do botão já anuncia o estado) e evita anúncio duplicado no leitor de tela.
52
+ - O traço usa `currentColor`; `color="current"` é o caminho para casar com a cor do container.
@@ -0,0 +1,192 @@
1
+ # Table — primitiva burra de grid
2
+
3
+ `Table` é a **primitiva de baixo nível** que renderiza estrutura visual de grid: head, body, rows, cells. Sem lógica de dados. Use-a quando precisar de uma tabela leve sem o overhead do `DataTable` (smart wrapper que orquestra filtro/sort/pagination/virtualization).
4
+
5
+ ## Anatomia
6
+
7
+ ```tsx
8
+ <Table density="standard" cellBorders ariaLabel="Lista de itens">
9
+ <TableHead>
10
+ <TableHeadCell field="id" sortable sortDirection="asc" icon={Hash} width={120}>
11
+ ID
12
+ </TableHeadCell>
13
+ <TableHeadCell field="name" sortable resizable width={240}>
14
+ Nome
15
+ </TableHeadCell>
16
+ </TableHead>
17
+
18
+ <TableBody>
19
+ {rows.map((row) => (
20
+ <TableRow key={row.id} selected={isSelected(row)} onClick={() => open(row)}>
21
+ <TableCell field="id" width={120}>{row.id}</TableCell>
22
+ <TableCell field="name" width={240} ellipsis>{row.name}</TableCell>
23
+ </TableRow>
24
+ ))}
25
+ </TableBody>
26
+ </Table>
27
+ ```
28
+
29
+ ## Componentes
30
+
31
+ | Componente | Responsabilidade |
32
+ |---|---|
33
+ | `Table` | Root grid container. Aceita `scrollRef` externa (pra virtualization). Expõe `cardBreakpoint?: number \| false` (default `768`) — hoje **no-op** no Table puro (ver Pattern 4). |
34
+ | `TableHead` | Sticky row group de headers. `sticky` toggle. `rootProps` pra HTMLAttributes. |
35
+ | `TableHeadCell` | Cell de header. `forwardRef`. Suporta sort, resize, icon, headMenu slot. |
36
+ | `TableBody` | Row group de body. `virtualized={{ totalHeight }}` ativa modo absolute layout. |
37
+ | `TableRow` | `forwardRef`. `selected`/`open`/`focused`/`clickable` variants. |
38
+ | `TableCell` | `forwardRef`. Cell de dados. `purpose="selection"` remove padding (checkbox). |
39
+ | `TableCardRow` | Modo card (mobile) — substitui TableRow inteira por card vertical. Pra auto-switch baseado em viewport, prefira `<DataTable cardBreakpoint={...}>` (faz o mapeamento automaticamente). |
40
+
41
+ ## Hooks expostos
42
+
43
+ | Hook | Pra que serve |
44
+ |---|---|
45
+ | `useColumnWidths(columns)` | Calcula `widths` efetivos + `offsets` cumulativos pra colunas pinned. |
46
+ | `useColumnResize({ currentWidth, onResize, onResizeEnd, onResizeLiveDOM })` | Drag-to-resize handle. 3 callbacks: live DOM (síncrono), onResize (mousemove), onResizeEnd (mouseup). |
47
+
48
+ ## Constants
49
+
50
+ ```ts
51
+ import { SELECTION_COLUMN_WIDTH, TABLE_HEADER_HEIGHT } from "@/components/ui/Table";
52
+
53
+ // SELECTION_COLUMN_WIDTH = 56 — usar em TableHeadCell/TableCell width
54
+ // TABLE_HEADER_HEIGHT = 42 — alinhar skeletons, totalizers, group headers
55
+ ```
56
+
57
+ ## Patterns
58
+
59
+ ### 1. Coluna de seleção (checkbox)
60
+
61
+ ```tsx
62
+ import { SELECTION_COLUMN_WIDTH } from "@/components/ui/Table";
63
+
64
+ <TableHeadCell width={SELECTION_COLUMN_WIDTH} purpose="selection">
65
+ <Checkbox checked={...} onCheckedChange={...} />
66
+ </TableHeadCell>
67
+
68
+ <TableCell width={SELECTION_COLUMN_WIDTH} purpose="selection">
69
+ <Checkbox checked={...} onCheckedChange={...} />
70
+ </TableCell>
71
+ ```
72
+
73
+ `purpose="selection"` é o **padrão pra coluna de checkbox**: remove o padding interno **e centraliza o checkbox** (header + body) automaticamente nos 56px — **não precisa** passar `align="center"`. Vale tanto no `<TableHeadCell>` quanto no `<TableCell>`. Use `width={SELECTION_COLUMN_WIDTH}` (56px) pra largura.
74
+
75
+ ### 2. Resize column com state externo
76
+
77
+ ```tsx
78
+ const [colWidths, setColWidths] = useState<Record<string, number>>({});
79
+
80
+ <TableHeadCell
81
+ field="name"
82
+ width={colWidths.name ?? 240}
83
+ resizable
84
+ // mousemove → side-effect (live DOM já é feito internamente pelo TableHeadCell)
85
+ onResize={(w) => console.log("dragging:", w)}
86
+ // mouseup → commit no state
87
+ onResizeEnd={(w) => setColWidths((prev) => ({ ...prev, name: w }))}
88
+ >
89
+ Nome
90
+ </TableHeadCell>
91
+ ```
92
+
93
+ ### 3. Virtualization (`@tanstack/react-virtual`)
94
+
95
+ ```tsx
96
+ const scrollRef = useRef<HTMLDivElement | null>(null);
97
+ const virtualizer = useVirtualizer({
98
+ count: rows.length,
99
+ getScrollElement: () => scrollRef.current,
100
+ estimateSize: () => 56,
101
+ overscan: 10,
102
+ });
103
+
104
+ <Table scrollRef={scrollRef}>
105
+ <TableHead>{/* ... */}</TableHead>
106
+ <TableBody virtualized={{ totalHeight: virtualizer.getTotalSize() }}>
107
+ {virtualizer.getVirtualItems().map((vi) => (
108
+ <TableRow
109
+ key={rows[vi.index].id}
110
+ rootProps={{
111
+ style: {
112
+ position: "absolute",
113
+ top: 0,
114
+ left: 0,
115
+ width: "100%",
116
+ height: `${vi.size}px`,
117
+ transform: `translateY(${vi.start}px)`,
118
+ },
119
+ }}
120
+ >
121
+ {/* cells */}
122
+ </TableRow>
123
+ ))}
124
+ </TableBody>
125
+ </Table>
126
+ ```
127
+
128
+ ### 4. Card mode manual (TableCardRow + matchMedia)
129
+
130
+ Pro `<Table>` puro, o auto-switch row→card NÃO é automático — você decide via `matchMedia` ou ResizeObserver:
131
+
132
+ > ⚠️ `TableProps` expõe `cardBreakpoint?: number | false` (default `768`), mas no `<Table>` puro a prop é **no-op**: ela só escreve a CSS var `--table-card-bp` no root — nenhuma regra CSS/`@container` consome essa var hoje. Não passe `cardBreakpoint` no `<Table>` esperando card mode automático; o auto-switch real é o do `<DataTable cardBreakpoint={...}>`, implementado via JS.
133
+
134
+ ```tsx
135
+ import { useMediaQuery } from "@/components/ui/MenuSidebar/use-media-query";
136
+ import { TableCardRow } from "@/components/ui/Table";
137
+
138
+ const isMobile = useMediaQuery("(max-width: 767px)");
139
+
140
+ return isMobile ? (
141
+ <div className="flex flex-col gap-gp-md">
142
+ {rows.map((row) => (
143
+ <TableCardRow
144
+ key={row.id}
145
+ header={<strong>{row.name}</strong>}
146
+ items={[
147
+ { label: "Email", value: row.email },
148
+ { label: "Status", value: row.status },
149
+ ]}
150
+ />
151
+ ))}
152
+ </div>
153
+ ) : (
154
+ <Table>{/* ... */}</Table>
155
+ );
156
+ ```
157
+
158
+ > Pra evitar o trabalho manual, use `<DataTable cardBreakpoint={768}>` — ele mapeia automaticamente as colunas (selection + primary + actions + items) e renderiza `<TableCardRow>` abaixo do breakpoint.
159
+
160
+ ### 5. Sticky pinned columns
161
+
162
+ ```tsx
163
+ const cols = [
164
+ { field: "id", width: 120, pinned: "left" as const },
165
+ { field: "name", width: 240, pinned: "left" as const },
166
+ { field: "email", width: 240 },
167
+ ];
168
+
169
+ const { widths, offsets } = useColumnWidths(cols);
170
+
171
+ <TableHeadCell field="id" width={widths.id} pinned="left" pinOffset={offsets.id}>
172
+ ID
173
+ </TableHeadCell>
174
+ ```
175
+
176
+ ## Table vs DataTable — quando usar qual
177
+
178
+ | Use **Table** quando | Use **DataTable** quando |
179
+ |---|---|
180
+ | Tabela estática sem filtros/sort/paginação | CRM/admin com filtros, sort, etc |
181
+ | Lista simples controlada por estado próprio | Server mode ou client mode com mock |
182
+ | Quer composição máxima manual | Quer setup rápido via prop config |
183
+ | Tabela dentro de modal/drawer pequeno | Página inteira com toolbar + footer |
184
+
185
+ ## Princípio dumb
186
+
187
+ `Table` **não armazena estado de dados**. Tudo é controlado externamente:
188
+ - Selection → consumer mantém `Set<id>` e passa pra cada `TableRow`
189
+ - Sort → consumer decide `sortDirection` por cell e implementa onClick
190
+ - Pagination → consumer fatia rows antes de mapear
191
+
192
+ O Table guarda **apenas** estado visual interno (resize hover, scroll detection) — via `TableContext` privado, nunca exposto.
@@ -0,0 +1,87 @@
1
+ # TableToolbar — toolbar opinativa de tabela
2
+
3
+ Toolbar **padrão** (e única) do DataTable. Layout OPINATIVO: slots semânticos em
4
+ ordem fixa — o consumer não monta a ordem, impossível montar errado.
5
+
6
+ ## Quando usar
7
+
8
+ - Você está montando uma toolbar de tabela custom (fora do DataTable) e quer o
9
+ visual/UX canônico do DS.
10
+ - Dentro do DataTable é automático — não precisa instanciar à mão.
11
+
12
+ ## Ordem renderizada (fixa)
13
+
14
+ ```
15
+ Esquerda: viewToggle · ⟨divider⟩ · savedViews
16
+ Direita: refresh · actions · search · filter · settings · more
17
+ ```
18
+
19
+ Em `< md`: o `refresh` some (o menu Configurações cobre o resto).
20
+
21
+ ## Slots (props)
22
+
23
+ | Slot | O que vai aqui |
24
+ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
25
+ | `viewToggle` | Toggle Kanban/Lista (`<ToolbarSegmented>`) |
26
+ | `savedViews` | Abas de visões + adicionar (`<TableToolbarViews>` + `<ViewsPopover>`). `<TableToolbarViews allowCreate={false}>` (v0.23.0) esconde o "+" e o modal de nova visão → só as visões pré-definidas (read-only). No DataTable use a prop `allowCreateView`. **`maxTabs`** (default `3`, **incluindo a aba "Default"**) limita quantas viram aba — o excedente é cortado por `.slice()`, sem overflow — mas em DEV sai um `console.warn` nomeando as visões engolidas e o valor que resolve (v0.43.1); no DataTable a prop equivalente é `maxViewTabs`. |
27
+ | `refresh` | Botão atualizar (`<ToolbarToolButton>`) |
28
+ | `search` | Campo de busca (`<ToolbarSearch>`) |
29
+ | `filter` | Filtro simples: use `<ToolbarFilterButton onClick isActive hasIndicator />` (funil, icon-only — **mesmo botão** no DataTable e DataList) → abre `<ToolbarSimpleFilterDrawer>` |
30
+ | `actions` | Ações custom: `<ToolbarActions actions={[...]} />` (`button`/`dropdown`/`input`). Inline no desktop; **colapsam no ⋯ no mobile** (passe `extraItems` p/ absorver o ⋯ existente) |
31
+ | `settings` | Configurações: sliders → drill-down (`<ToolbarSettingsMenu>` com Ordenação · Colunas · Filtros avançados · Densidade) |
32
+ | `more` | Menu "⋯" (`<MoreMenu>`) — export + ações |
33
+ | `bulkBar` | Substitui a toolbar inteira pela barra de ações em massa |
34
+ | `className` | — |
35
+
36
+ ## Exemplo mínimo
37
+
38
+ ```tsx
39
+ import {
40
+ TableToolbar,
41
+ ToolbarSearch,
42
+ ToolbarToolButton,
43
+ ToolbarSettingsMenu,
44
+ ToolbarSegmented,
45
+ ToolbarApplied,
46
+ MoreMenu,
47
+ SortPanel,
48
+ ColsPanel,
49
+ FilterPanel,
50
+ } from "@/components/ui/TableToolbar";
51
+
52
+ <TableToolbar
53
+ search={<ToolbarSearch value={q} onChange={(e) => setQ(e.target.value)} />}
54
+ settings={
55
+ <ToolbarSettingsMenu
56
+ trigger={<ToolbarToolButton icon={<SlidersHorizontal />} aria-label="Configurações da tabela" />}
57
+ sortPanel={(onBack) => <SortPanel ... onBack={onBack} />}
58
+ colsPanel={(onBack) => <ColsPanel ... onBack={onBack} />}
59
+ filterPanel={(onBack) => <FilterPanel ... onBack={onBack} />}
60
+ density={<ToolbarSegmented items={DENSITY} ... />}
61
+ />
62
+ }
63
+ more={<MoreMenu ... />}
64
+ />
65
+ {/* Chips dos filtros aplicados LOGO ABAIXO */}
66
+ <ToolbarApplied filters={appliedFilters} onRemove={...} />
67
+ ```
68
+
69
+ ## Gotchas
70
+
71
+ - **Ordem é fixa** — não dá (nem precisa) reordenar. Cada controle tem seu slot semântico.
72
+ - **Mobile**: o `refresh` é escondido em `< md` via `hidden md:contents`; o
73
+ `<ToolbarSettingsMenu>` expõe Visualização/Visões inline dentro do menu nesse breakpoint.
74
+ - **`mobileDisplayToggle?: ReactNode`** (ToolbarSettingsMenu): slot pro toggle
75
+ **"Exibição" (Linhas/Cards)** — só renderiza em `< md`. O DataTable passa um
76
+ `<ToolbarSegmented>` aqui quando a tela cabe em card mode; permite o usuário
77
+ forçar tabela mesmo abaixo do `cardBreakpoint` (default mobile = **tabela**).
78
+ - **bulkBar substitui tudo**: passe `<BulkActionsBar>` (auto-some quando `count=0`)
79
+ condicionalmente — `selectedIds.size > 0 ? <BulkActionsBar/> : undefined`.
80
+ - **Chips ficam fora** da toolbar: renderize `<ToolbarApplied>` como irmão abaixo.
81
+
82
+ ## Acoplamento DataTable ↔ TableToolbar
83
+
84
+ O DataTable consome os parts do TableToolbar via composição e orquestra os slots
85
+ na ordem fixa. Mudar a ordem/conteúdo dos controles do DataTable é edição **no
86
+ DataTable**, não aqui. Coupling reverso (DataTable → TableToolbar para
87
+ `columnTypeRegistry`/`FilterModel`) é aceito; o inverso é proibido.