@snksergio/design-system 0.61.0 → 0.63.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 (193) hide show
  1. package/dist-lib/ai/blocos/chart/budget-breakdown.tsx +174 -0
  2. package/dist-lib/ai/blocos/indice.md +43 -0
  3. package/dist-lib/ai/blocos/paneldetail/detalhe-com-tabela.tsx +450 -0
  4. package/dist-lib/ai/blocos/paneldetail/detalhe-de-tarefa-com-abas.tsx +570 -0
  5. package/dist-lib/ai/blocos/paneldetail/detalhe-do-registro.tsx +555 -0
  6. package/dist-lib/ai/componentes/AlertModal.md +47 -0
  7. package/dist-lib/ai/componentes/AppShell.md +117 -0
  8. package/dist-lib/ai/componentes/Breadcrumb.md +187 -0
  9. package/dist-lib/ai/componentes/Button.md +116 -0
  10. package/dist-lib/ai/componentes/ButtonGroup.md +162 -0
  11. package/dist-lib/ai/componentes/CardCheckbox.md +99 -0
  12. package/dist-lib/ai/componentes/CardOption.md +133 -0
  13. package/dist-lib/ai/componentes/Chart.md +93 -0
  14. package/dist-lib/ai/componentes/Chip.md +68 -0
  15. package/dist-lib/ai/componentes/ChoroplethMap.md +118 -0
  16. package/dist-lib/ai/componentes/ColorPicker.md +70 -0
  17. package/dist-lib/ai/componentes/Combobox.md +51 -0
  18. package/dist-lib/ai/componentes/ConversationListItem.md +90 -0
  19. package/dist-lib/ai/componentes/DataList.md +111 -0
  20. package/dist-lib/ai/componentes/DataTable.md +867 -0
  21. package/dist-lib/ai/componentes/DatePicker.md +84 -0
  22. package/dist-lib/ai/componentes/DateSeparatorChip.md +59 -0
  23. package/dist-lib/ai/componentes/EmptyState.md +72 -0
  24. package/dist-lib/ai/componentes/FileUploadField.md +95 -0
  25. package/dist-lib/ai/componentes/FloatingPanel.md +118 -0
  26. package/dist-lib/ai/componentes/FooterTable.md +62 -0
  27. package/dist-lib/ai/componentes/FormField.md +110 -0
  28. package/dist-lib/ai/componentes/Gantt.md +552 -0
  29. package/dist-lib/ai/componentes/Header.md +98 -0
  30. package/dist-lib/ai/componentes/Icon.md +65 -0
  31. package/dist-lib/ai/componentes/Kanban.md +343 -0
  32. package/dist-lib/ai/componentes/Kpi.md +103 -0
  33. package/dist-lib/ai/componentes/List.md +61 -0
  34. package/dist-lib/ai/componentes/MarkdownText.md +59 -0
  35. package/dist-lib/ai/componentes/MenuSidebar.md +128 -0
  36. package/dist-lib/ai/componentes/MessageAck.md +53 -0
  37. package/dist-lib/ai/componentes/MessageBubble.md +115 -0
  38. package/dist-lib/ai/componentes/MessageComposer.md +80 -0
  39. package/dist-lib/ai/componentes/MessageVariablesPicker.md +104 -0
  40. package/dist-lib/ai/componentes/Modal.md +88 -0
  41. package/dist-lib/ai/componentes/MonthYearPicker.md +49 -0
  42. package/dist-lib/ai/componentes/PageHeader.md +129 -0
  43. package/dist-lib/ai/componentes/Panel.md +84 -0
  44. package/dist-lib/ai/componentes/Scheduler.md +421 -0
  45. package/dist-lib/ai/componentes/ScreenLoader.md +60 -0
  46. package/dist-lib/ai/componentes/SingleMenuSidebar.md +171 -0
  47. package/dist-lib/ai/componentes/Spinner.md +52 -0
  48. package/dist-lib/ai/componentes/Table.md +192 -0
  49. package/dist-lib/ai/componentes/TableToolbar.md +87 -0
  50. package/dist-lib/ai/componentes/TabsNavigation.md +152 -0
  51. package/dist-lib/ai/componentes/Toast.md +49 -0
  52. package/dist-lib/ai/componentes/_primitivos.md +74 -0
  53. package/dist-lib/ai/componentes/avatar-ig.md +181 -0
  54. package/dist-lib/ai/componentes/indice.json +49 -0
  55. package/dist-lib/ai/exemplos/app-shell/app-shell-example.tsx +140 -0
  56. package/dist-lib/ai/exemplos/app-shell/index.ts +3 -0
  57. package/dist-lib/ai/exemplos/app-shell/nav-data.ts +97 -0
  58. package/dist-lib/ai/exemplos/app-shell/routes.tsx +75 -0
  59. package/dist-lib/ai/exemplos/chat/chat-screen.tsx +152 -0
  60. package/dist-lib/ai/exemplos/chat/chat-v2-mocks.ts +171 -0
  61. package/dist-lib/ai/exemplos/chat/chat-v2.styles.ts +23 -0
  62. package/dist-lib/ai/exemplos/chat/chat-v2.types.ts +81 -0
  63. package/dist-lib/ai/exemplos/chat/components/ChannelDot/channel-dot.tsx +28 -0
  64. package/dist-lib/ai/exemplos/chat/components/ConversationActionsMenu/conversation-actions-menu.tsx +83 -0
  65. package/dist-lib/ai/exemplos/chat/components/ConversationActionsMenu/index.ts +4 -0
  66. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.styles.ts +22 -0
  67. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.tsx +203 -0
  68. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.types.ts +10 -0
  69. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/index.ts +2 -0
  70. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.styles.ts +55 -0
  71. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.tsx +52 -0
  72. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.types.ts +7 -0
  73. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/index.ts +2 -0
  74. package/dist-lib/ai/exemplos/chat/components/DateSeparator/date-separator.tsx +18 -0
  75. package/dist-lib/ai/exemplos/chat/components/DetailField/detail-field.tsx +23 -0
  76. package/dist-lib/ai/exemplos/chat/components/DetailSection/detail-section.tsx +41 -0
  77. package/dist-lib/ai/exemplos/chat/components/DetailSection/index.ts +1 -0
  78. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.styles.ts +30 -0
  79. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.tsx +142 -0
  80. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.types.ts +16 -0
  81. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/index.ts +5 -0
  82. package/dist-lib/ai/exemplos/chat/components/FilterRow/filter-row.styles.ts +37 -0
  83. package/dist-lib/ai/exemplos/chat/components/FilterRow/filter-row.tsx +45 -0
  84. package/dist-lib/ai/exemplos/chat/components/FilterRow/index.ts +1 -0
  85. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.styles.ts +27 -0
  86. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.tsx +66 -0
  87. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.types.ts +13 -0
  88. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-rail.tsx +38 -0
  89. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/index.ts +6 -0
  90. package/dist-lib/ai/exemplos/chat/components/MessageBubble/index.ts +2 -0
  91. package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.styles.ts +37 -0
  92. package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.tsx +29 -0
  93. package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.types.ts +5 -0
  94. package/dist-lib/ai/exemplos/chat/components/PersonAvatar/index.ts +5 -0
  95. package/dist-lib/ai/exemplos/chat/components/PersonAvatar/person-avatar.tsx +31 -0
  96. package/dist-lib/ai/exemplos/chat/components/QueueColumn/index.ts +2 -0
  97. package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.styles.ts +19 -0
  98. package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.tsx +140 -0
  99. package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.types.ts +13 -0
  100. package/dist-lib/ai/exemplos/chat/components/RailItem/index.ts +1 -0
  101. package/dist-lib/ai/exemplos/chat/components/RailItem/rail-item.styles.ts +30 -0
  102. package/dist-lib/ai/exemplos/chat/components/RailItem/rail-item.tsx +42 -0
  103. package/dist-lib/ai/exemplos/chat/hooks/use-resizable.ts +98 -0
  104. package/dist-lib/ai/exemplos/chat/index.ts +1 -0
  105. package/dist-lib/ai/exemplos/clientes/_table-data.ts +59 -0
  106. package/dist-lib/ai/exemplos/clientes/clientes-screen.tsx +505 -0
  107. package/dist-lib/ai/exemplos/clientes/clientes-showcase-mocks.ts +117 -0
  108. package/dist-lib/ai/exemplos/clientes/clientes-showcase.styles.ts +13 -0
  109. package/dist-lib/ai/exemplos/clientes/clientes-showcase.types.ts +16 -0
  110. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.styles.ts +28 -0
  111. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.tsx +303 -0
  112. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.types.ts +14 -0
  113. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/index.ts +2 -0
  114. package/dist-lib/ai/exemplos/clientes/components/NovoClienteDrawer/index.ts +2 -0
  115. package/dist-lib/ai/exemplos/clientes/components/NovoClienteDrawer/novo-cliente-drawer.tsx +207 -0
  116. package/dist-lib/ai/exemplos/clientes/index.ts +1 -0
  117. package/dist-lib/ai/exemplos/dashboard/dashboard-brazil-map.ts +33 -0
  118. package/dist-lib/ai/exemplos/dashboard/dashboard-screen.tsx +1110 -0
  119. package/dist-lib/ai/exemplos/dashboard/index.ts +1 -0
  120. package/dist-lib/ai/exemplos/edit-page/components/StepNav.tsx +81 -0
  121. package/dist-lib/ai/exemplos/edit-page/components/section-card.tsx +85 -0
  122. package/dist-lib/ai/exemplos/edit-page/edit-page-screen.tsx +234 -0
  123. package/dist-lib/ai/exemplos/edit-page/index.ts +1 -0
  124. package/dist-lib/ai/exemplos/finance/_table-data.ts +57 -0
  125. package/dist-lib/ai/exemplos/finance/clientes-financeiro-mocks.ts +227 -0
  126. package/dist-lib/ai/exemplos/finance/clientes-financeiro.types.ts +75 -0
  127. package/dist-lib/ai/exemplos/finance/clientes-showcase-mocks.ts +117 -0
  128. package/dist-lib/ai/exemplos/finance/clientes-showcase.styles.ts +13 -0
  129. package/dist-lib/ai/exemplos/finance/clientes-showcase.types.ts +16 -0
  130. package/dist-lib/ai/exemplos/finance/components/EditarFinanceDrawer/editar-finance-drawer.tsx +241 -0
  131. package/dist-lib/ai/exemplos/finance/components/EditarFinanceDrawer/index.ts +5 -0
  132. package/dist-lib/ai/exemplos/finance/components/ExtratoExpansion/extrato-expansion.tsx +172 -0
  133. package/dist-lib/ai/exemplos/finance/components/ExtratoExpansion/index.ts +1 -0
  134. package/dist-lib/ai/exemplos/finance/components/FinanceDetailPanel/finance-detail-panel.tsx +241 -0
  135. package/dist-lib/ai/exemplos/finance/components/FinanceDetailPanel/index.ts +2 -0
  136. package/dist-lib/ai/exemplos/finance/components/NovoClienteDrawer/index.ts +2 -0
  137. package/dist-lib/ai/exemplos/finance/components/NovoClienteDrawer/novo-cliente-drawer.tsx +207 -0
  138. package/dist-lib/ai/exemplos/finance/components/SacarDialog/index.ts +2 -0
  139. package/dist-lib/ai/exemplos/finance/components/SacarDialog/sacar-dialog.tsx +346 -0
  140. package/dist-lib/ai/exemplos/finance/finance-screen.tsx +821 -0
  141. package/dist-lib/ai/exemplos/finance/index.ts +1 -0
  142. package/dist-lib/ai/exemplos/gantt/_gantt-data.tsx +402 -0
  143. package/dist-lib/ai/exemplos/gantt/gantt-screen.tsx +512 -0
  144. package/dist-lib/ai/exemplos/gantt/index.ts +1 -0
  145. package/dist-lib/ai/exemplos/login/index.ts +1 -0
  146. package/dist-lib/ai/exemplos/login/login-screen.tsx +246 -0
  147. package/dist-lib/ai/exemplos/mapa-rede/components/ConsultorDetailPanel/consultor-detail-panel.tsx +158 -0
  148. package/dist-lib/ai/exemplos/mapa-rede/components/ConsultorDetailPanel/index.ts +2 -0
  149. package/dist-lib/ai/exemplos/mapa-rede/index.ts +1 -0
  150. package/dist-lib/ai/exemplos/mapa-rede/mapa-de-rede-mocks.ts +655 -0
  151. package/dist-lib/ai/exemplos/mapa-rede/mapa-de-rede.types.ts +53 -0
  152. package/dist-lib/ai/exemplos/mapa-rede/mapa-rede-screen.tsx +227 -0
  153. package/dist-lib/ai/exemplos/order-detail/components/ActivityTab.tsx +77 -0
  154. package/dist-lib/ai/exemplos/order-detail/components/AttachmentsTab.tsx +45 -0
  155. package/dist-lib/ai/exemplos/order-detail/components/CommentsTab.tsx +111 -0
  156. package/dist-lib/ai/exemplos/order-detail/components/DetailsTab.tsx +189 -0
  157. package/dist-lib/ai/exemplos/order-detail/components/OverviewTab.tsx +200 -0
  158. package/dist-lib/ai/exemplos/order-detail/components/section-card.tsx +85 -0
  159. package/dist-lib/ai/exemplos/order-detail/index.ts +1 -0
  160. package/dist-lib/ai/exemplos/order-detail/order-detail-screen.tsx +119 -0
  161. package/dist-lib/ai/exemplos/order-detail/order-mocks.ts +186 -0
  162. package/dist-lib/ai/exemplos/order-detail/order.types.ts +118 -0
  163. package/dist-lib/ai/global/componentes.md +217 -0
  164. package/dist-lib/ai/global/composicao.md +182 -0
  165. package/dist-lib/ai/indice.json +192 -0
  166. package/dist-lib/ai/lint/ds-lint-patterns.mjs +115 -0
  167. package/dist-lib/ai/manifest.json +42 -0
  168. package/dist-lib/ai/regras/design.md +88 -0
  169. package/dist-lib/ai/regras/temas.md +192 -0
  170. package/dist-lib/ai/regras-por-componente.json +103 -0
  171. package/dist-lib/ai/roteiros/app-builder/roteiro.md +155 -0
  172. package/dist-lib/ai/roteiros/auth-builder/roteiro.md +42 -0
  173. package/dist-lib/ai/roteiros/cards/roteiro.md +34 -0
  174. package/dist-lib/ai/roteiros/charts/roteiro.md +32 -0
  175. package/dist-lib/ai/roteiros/chat/roteiro.md +31 -0
  176. package/dist-lib/ai/roteiros/crud-builder/blueprint.md +63 -0
  177. package/dist-lib/ai/roteiros/crud-builder/entrevista.md +139 -0
  178. package/dist-lib/ai/roteiros/crud-builder/geracao.md +113 -0
  179. package/dist-lib/ai/roteiros/crud-builder/roteiro.md +89 -0
  180. package/dist-lib/ai/roteiros/dashboard-builder/blueprint.md +47 -0
  181. package/dist-lib/ai/roteiros/dashboard-builder/entrevista.md +62 -0
  182. package/dist-lib/ai/roteiros/dashboard-builder/geracao.md +88 -0
  183. package/dist-lib/ai/roteiros/dashboard-builder/roteiro.md +88 -0
  184. package/dist-lib/ai/roteiros/drawers/roteiro.md +41 -0
  185. package/dist-lib/ai/roteiros/list-builder/blueprint.md +94 -0
  186. package/dist-lib/ai/roteiros/list-builder/entrevista.md +188 -0
  187. package/dist-lib/ai/roteiros/list-builder/geracao.md +63 -0
  188. package/dist-lib/ai/roteiros/list-builder/roteiro.md +91 -0
  189. package/dist-lib/ai/roteiros/module-replicator/roteiro.md +56 -0
  190. package/dist-lib/ai/roteiros/page-detail/roteiro.md +32 -0
  191. package/dist-lib/ai/roteiros/page-edit/roteiro.md +31 -0
  192. package/dist-lib/ai/roteiros/screen-composer/roteiro.md +95 -0
  193. package/package.json +4 -1
@@ -0,0 +1,867 @@
1
+ # DataTable — Guia de uso
2
+
3
+ <!-- ds:regras
4
+ - filtro é NATIVO e reativo: `enableColumnFilter` na coluna — nunca select/form solto acima da grade. Pra ele aparecer desde o load (e não ficar escondido atrás do ícone), `showEmptyFilterChips={["status", …]}`
5
+ - não fixe `width`: com `autoFit` (default) ele é PISO e entra no rateio, não trava. Travar de verdade = `width` + `maxWidth` iguais
6
+ - vazio são DOIS casos distintos: sem dado nenhum → `renderEmpty` (CTA de criar); filtro/busca zerou → `renderNoResults` (o "limpar filtros" já vem cabeado)
7
+ -->
8
+
9
+ Wrapper smart sobre `<TableToolbar>` + `<Table>` + `<FooterTable>` que orquestra **17 hooks SRP** (sort, filter, search, pagination, selection, visibility, density, processor, query, export, saved views, persistence, etc) e renderiza body com suporte a virtualização, agrupamento e expansão.
10
+
11
+ > **Princípio**: o DataTable é smart, mas cada primitive (Table, TableToolbar, FooterTable) é dumb e standalone. Veja `Table/USAGE.md` e `TableToolbar/USAGE.md` se quiser montar uma tabela custom fora do DataTable.
12
+
13
+ ---
14
+
15
+ ## Imports
16
+
17
+ ```tsx
18
+ import {
19
+ DataTable,
20
+ type DataTableColumnDef,
21
+ type DataTableRef,
22
+ // builders pra reduzir boilerplate:
23
+ textColumn,
24
+ currencyColumn,
25
+ dateColumn,
26
+ statusColumn,
27
+ actionColumn,
28
+ // registry pra tipos custom:
29
+ columnTypeRegistry,
30
+ type ColumnTypeDefinition,
31
+ } from "@/components/ui/DataTable";
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Quick start — client mode (CRUD)
37
+
38
+ ```tsx
39
+ interface Client {
40
+ id: number;
41
+ name: string;
42
+ email: string;
43
+ status: "active" | "inactive";
44
+ value: number;
45
+ createdAt: string;
46
+ }
47
+
48
+ // ⚠️ Nenhuma coluna fixa `width` de propósito: com `autoFit` (default) o DataTable
49
+ // mede o conteúdo e distribui o espaço. `width` aqui seria PISO, não trava — fixar
50
+ // em todas só desloca o ponto de partida do rateio. Trave uma coluna só quando
51
+ // precisar, com `width` + `maxWidth` iguais.
52
+ const columns = useMemo<DataTableColumnDef<Client>[]>(
53
+ () => [
54
+ textColumn<Client>("id", "ID"),
55
+ textColumn<Client>("name", "Nome", { sortable: true }),
56
+ { field: "email", headerName: "Email", type: "email" },
57
+ currencyColumn<Client>("value", "Valor", { currency: "BRL" }),
58
+ dateColumn<Client>("createdAt", "Criado em"),
59
+ statusColumn<Client>("status", "Status", [
60
+ { value: "active", label: "Ativo", color: "success" },
61
+ { value: "inactive", label: "Inativo", color: "muted" },
62
+ ]),
63
+ actionColumn<Client>({
64
+ getActions: ({ row }) => [
65
+ { label: "Editar", onClick: () => editClient(row) },
66
+ {
67
+ label: "Excluir",
68
+ onClick: () => removeClient(row),
69
+ destructive: true,
70
+ },
71
+ ],
72
+ }),
73
+ ],
74
+ [editClient, removeClient],
75
+ );
76
+
77
+ <DataTable<Client>
78
+ rows={clients}
79
+ columns={columns}
80
+ toolbar={{ title: "Clientes", enableSearch: true, enableFilters: true }}
81
+ paginationConfig={{ enabled: true, initialPageSize: 25 }}
82
+ selectionConfig={{ enabled: true, enableGlobal: true }}
83
+ onRowClick={(row) => router.push(`/clients/${row.id}`)}
84
+ />;
85
+ ```
86
+
87
+ > `columns` **deve** ser memoizado — o processor reage à identidade do array, não ao conteúdo.
88
+
89
+ ---
90
+
91
+ ## Capacidades
92
+
93
+ | Capability | Como ativar |
94
+ | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
95
+ | **Sort multi** | `sortable: true` na coluna; toolbar Sort popover surge automaticamente |
96
+ | **Filter chip rápido** | `enableColumnFilter: true` + `filterType: "text"\|"number"\|"date"\|"select"\|"multiSelect"\|"boolean"` |
97
+ | **Filter avançado (AND/OR)** | Habilitado por default se houver coluna com `enableColumnFilter` |
98
+ | **Filter chips placeholder** | `showEmptyFilterChips={["status", "categoria"]}` — chips nativos visíveis desde o load inicial, mesmo sem valor preenchido (user clica e preenche) |
99
+ | **Search global** | `toolbar.enableSearch: true` (default). Client mode busca em todos os fields; server mode recebe `search` (debounced) + `searchField?` no `GridFetchParams` |
100
+ | **Pagination** | `paginationConfig.enabled: true` (default) |
101
+ | **Selection (bulk)** | `selectionConfig.enabled: true` |
102
+ | **Visibility / pin / reorder** | `toolbar.enableColumns: true` (default) |
103
+ | **Density toggle** | `toolbar.enableDensity: true` (default). Override items via `densityItems` prop |
104
+ | **Column types registry** | `type: "currency"` etc — renderiza display + filter input via registry |
105
+ | **Formato de data** | `type: "date"` → `14/03/2023` · `type: "datetime"` → `14/03/2023 09:30`. Outro formato? **`valueFormatter`** — ele vence o do tipo e vale na célula, no export e no clipboard. Não precisa de `render`. ⚠️ Até a v0.42.1 esses tipos formatavam **sem ano** ("14 de mar") e o `valueFormatter` **não alcançava a célula** (mudava só export/totalizador). |
106
+ | **Coluna de ações** | `type: "actions"` + `getActions` (ou o builder `actionColumn`). **É o `type` que dá as 3 garantias** — última coluna, ancorada à direita, largura fixa. Coluna montada na unha com botões não recebe nenhuma delas. Ver §Coluna de ações abaixo. |
107
+ | **Inline edit** | `editable: true` na coluna + `onCellEditCommit` |
108
+ | **Read-more (Ler mais)** | `readMore: true` na coluna (ou `{ lines?, label? }`) — trunca + popover com texto completo |
109
+ | **Copy célula** | `copyable: true` na coluna (ou `{ value?, label? }`) — ícone copiar no hover + feedback "Copiado!" (~2s) |
110
+ | **Grab-to-scroll horizontal** | **nativo (default `true`)** — arrastar o corpo (mouse/pen) rola lateralmente; `grabToScroll={false}` desliga |
111
+ | **Tela cheia (fullscreen)** | `toolbar.enableFullscreen: true` — botão ⤢ na toolbar expande a tabela pra viewport inteira (Esc volta) |
112
+ | **Ações custom no toolbar** | `toolbar.actions: ToolbarAction[]` — `button`/`dropdown`/`input` (ex.: seletor de período). Inline no desktop (entre Filtros e ⋯); no mobile colapsam num ⋯ próprio. Ver `<ToolbarActions>` no TableToolbar |
113
+ | **Server mode** | passe `fetchData` em vez de `rows` |
114
+ | **Card responsivo (mobile)** | `cardBreakpoint` (default 768). Abaixo desse valor o **default é tabela** (densidade > cards pra power user); o usuário alterna pra cards via toggle **"Exibição" (Linhas/Cards)** que aparece na ToolbarSettingsMenu (`mobileDisplayToggle`). `cardBreakpoint={false}` desabilita o card mode por completo. |
115
+ | **Toolbar responsiva (mobile)** | Em viewports `<md` (768px), controles secundários (sort / cols / density / refresh / view toggle / saved views / export / more menu) colapsam automaticamente num icon-button dropdown `...` via `ToolbarMobileDialog`. Search e Filter continuam sempre visíveis na linha principal. Comportamento built-in — sem prop necessária. |
116
+ | **Virtualização** | `virtualize: true` (+ `estimateRowHeight` / `overscan` opcionais) |
117
+ | **Row grouping** | `groupBy: "status"` (1 field na V1) + opcionais `renderGroupHeader`/`renderGroupContent` pra free-form |
118
+ | **Row expansion** | `expandable: true` na coluna + `renderRowExpansion: ({ row }) => <Detail row={row} />` |
119
+ | **Tree-data (hierarquia)** | `getTreeDataPath: (row) => [...]` + `treeColumn: true` na coluna primária. Rows continuam FLAT; o path define a árvore. Pagination desliga automaticamente. |
120
+ | **Saved views** | `savedViewsService` (use `savedViewsMockService` em dev) |
121
+ | **State persistence** | `persistId: "clients-table"` — workspace "Default" completo persiste em localStorage (sort, filter, search, page, density, column widths/pin/hide/order, viewMode, groupBy, expanded rows). Quando view custom está ativa, o snapshot da Default fica congelado — voltar para Default restaura tudo intacto. Limpeza manual via `ref.current.resetPersistedState()`. |
122
+ | **Auto-fit das colunas** | `autoFit: true` (default) — observa container via ResizeObserver, mede conteúdo das primeiras N rows (canvas) e distribui a sobra. ⚠️ **`col.width` é PISO, não trava** — a coluna entra no rateio e cresce a partir dele (medido: pedir 80/240/280 num container de 1400px devolve 187/560/653). Pra travar de verdade: **`width` + `maxWidth` iguais**. Prefira não fixar `width`. `autoFit={false}` desliga (legacy). |
123
+ | **Resize manual de colunas** | Default ativo em todas as colunas exceto `type: "actions"` ou `purpose: "selection"`. Drag handle aparece no edge direito do header. Limites hard `60–800px`; respeita `col.minWidth/maxWidth` quando definidos. Para desabilitar em uma coluna específica: `resizable: false`. |
124
+ | **Export** | `toolbar.enableExport: true` (CSV default com escopos all/filtered/selected) — formatos custom via `enableExport: { formats: [{ id, label, onSelect }] }` |
125
+ | **View Kanban (board)** | `viewMode="kanban"` (controlled) ou `defaultViewMode` (uncontrolled) + `kanbanConfig={{ groupByField, renderCard }}` — toggle table/kanban auto na toolbar |
126
+ | **View Lista (cards)** | `viewMode="list"` + `listConfig={{ renderItem(row) }}` — toggle Tabela/Lista auto na toolbar; mesma toolbar, corpo vira `<List>`. `hierarchical: true` + `getTreeDataPath` = lista em árvore. Showcase `#/clients-list-view` |
127
+ | **Totalizer row** | `showTotalizers` na DataTable + `aggregate: "sum"` (+ `aggregateFormatter`) na coluna; server mode pode sobrescrever via `aggregateRow` |
128
+ | **Keyboard navigation** | Auto — setas, Home/End, PgUp/PgDn no body |
129
+ | **Estados (vazio / carregando / sem resultado)** | Já vêm com default embutido; `loading: boolean` + `renderEmpty` / `renderLoading` / `renderNoResults` **substituem** (são `ReactNode`, não função). Ver §Estados abaixo |
130
+
131
+ ---
132
+
133
+ ## Receitas comuns
134
+
135
+ ### Estados: vazio, carregando, sem resultado
136
+
137
+ As três telas que aparecem quando não há linha pra mostrar. **Os três já têm default do
138
+ DS** — você só passa a prop pra substituir 100% do slot:
139
+
140
+ | Prop | Dispara quando | Default embutido |
141
+ |---|---|---|
142
+ | `renderLoading` | `loading` é `true` | spinner do DS |
143
+ | `renderEmpty` | o dataset não tem **nenhum** registro | `<DataTableEmpty />` — ilustração + título + descrição + ação opcional |
144
+ | `renderNoResults` | há linhas, mas **filtro/busca zerou** o resultado | `<DataTableNoResults />` com **`onClearFilters` já cabeado** pelo DataTable (limpa `filterModel` + `search`) |
145
+
146
+ ```tsx
147
+ <DataTable
148
+ rows={rows}
149
+ columns={columns}
150
+ loading={isFetching} // prop SEPARADA, boolean
151
+ renderLoading={<MeuSkeleton />} // ReactNode — não é função, não recebe props
152
+ renderEmpty={<EmptyState title="Nenhum cliente ainda" action={<Button>Novo</Button>} />}
153
+ renderNoResults={<EmptyState title="Nada encontrado" description="Ajuste os filtros." />}
154
+ />
155
+ ```
156
+
157
+ ⚠️ **Empty ≠ NoResults, e trocar os dois é o erro comum.** "Não existe nada ainda" pede
158
+ CTA de **criar**; "seu filtro não achou" pede **limpar filtro** — e nesse segundo caso o
159
+ default já entrega o botão certo, cabeado. Substituir por um `EmptyState` genérico
160
+ **perde** esse wiring: se for substituir o `renderNoResults`, cabeie você mesmo o clear.
161
+
162
+ ### Coluna de ações (editar / excluir / "…")
163
+
164
+ **Use `type: "actions"`.** É o `type` — não a posição no array, não `pinned` — que liga as
165
+ três garantias, todas resolvidas no `use-data-table-columns.ts`:
166
+
167
+ 1. **Vai pro fim** da tabela, mesmo se você declarar a coluna no meio do array.
168
+ 2. **Ancora à direita** (`pinned: "right"` implícito) — fica visível com scroll horizontal.
169
+ Passar `pinned: "right"` na mão é redundante.
170
+ 3. **Não entra no rateio do autoFit** — largura fixa, não estica junto das outras.
171
+
172
+ ```tsx
173
+ // builder (recomendado) — `field`/`headerName`/`type`/`pinned` já vêm certos
174
+ actionColumn<Client>({
175
+ getActions: ({ row }) => [
176
+ { id: "edit", label: "Editar", icon: <Pencil />, onClick: () => edit(row) },
177
+ { id: "del", label: "Excluir", icon: <Trash2 />, destructive: true, onClick: () => del(row) },
178
+ ],
179
+ }),
180
+ ```
181
+
182
+ ⛔ **Montar a coluna na unha perde as três.** `{ field: "acoes", headerName: "Ações",
183
+ render: () => <><Button/><Button/></> }` não tem `type`, então: não vai pro fim, não ancora,
184
+ **e entra no rateio** — medido, uma coluna assim com `width: 120` num container de 1400px
185
+ termina com **220px**, e o conteúdo alinhado à esquerda deixa os botões ~100px longe da
186
+ borda. É o sintoma "o botão de ação ficou no meio da tabela". Se precisa de render próprio,
187
+ mantenha `type: "actions"` e use `getActions` — `customColumn` **não** serve pra isso.
188
+
189
+ #### Quantas ações aparecem inline
190
+
191
+ | ações visíveis na row | render | largura |
192
+ |---|---|---|
193
+ | 1 | 1 ícone, sem "…" | 44px |
194
+ | 2 | 2 ícones | 74px |
195
+ | 3 | 3 ícones | 104px |
196
+ | **4+** | **só o "…"**, todas dentro | 44px |
197
+ | qualquer nº, com `showInMenu` em algum item | **o seu split**, sem limite | derivada |
198
+
199
+ O corte em 3 é a **capacidade geométrica** da coluna, não preferência: a célula usa
200
+ `px-pad-md` na variante `actions` (8×2 — sobrescreve o `px-pad-2xl` das outras) + ícone
201
+ `icon-2xs` (28px) + gap `gp-2xs` (2px) → `largura = 30n + 14`. Quatro ícones pedem 134px,
202
+ contra os 120 que a coluna sempre teve. Antes da v0.42.0 a largura era 120px **fixos** pra
203
+ qualquer quantidade: 1 ação reservava espaço pra 3, e 4 ícones vazavam a coluna.
204
+
205
+ Pra forçar um split diferente, marque `showInMenu: true` nos itens que devem ir pro menu —
206
+ isso desliga o automático e respeita você integralmente, inclusive com 5 ícones inline.
207
+
208
+ `hidden` é resolvido **por row antes de contar**: uma row com 4 ações onde uma está oculta
209
+ volta a renderizar 3 ícones inline. A largura reservada é o **máximo** entre as rows
210
+ amostradas, senão a row mais completa ficaria cortada.
211
+
212
+ **Largura:** `col.width` > `col.minWidth` > derivada da contagem. ⚠️ Até a v0.42.0 o
213
+ `col.width` era **ignorado** nesta coluna (só `minWidth` funcionava) — se você tem
214
+ `actionColumn({ width: 64 })` em código antigo, ele passa a valer de verdade agora, e 64px
215
+ comporta **1** ícone.
216
+
217
+ ### Server mode (refetch async + paginação remota)
218
+
219
+ ```tsx
220
+ const fetchData = useCallback(
221
+ async ({ pagination, sort, filters, search }: GridFetchParams) => {
222
+ const res = await api.get("/clients", {
223
+ params: serialize({ pagination, sort, filters, search }),
224
+ });
225
+ return { data: res.data.items, total: res.data.total }; // GridFetchResult<T>
226
+ },
227
+ [],
228
+ );
229
+
230
+ <DataTable<Client>
231
+ fetchData={fetchData}
232
+ columns={columns}
233
+ toolbar={{ enableSearch: true, enableFilters: true }}
234
+ paginationConfig={{ enabled: true, initialPageSize: 25 }}
235
+ />;
236
+ ```
237
+
238
+ `fetchData` é re-disparado quando muda `pagination | sort | filter | search`. Use ref/AbortController interno se precisar cancelar. Loading state é managed pelo controller (skeleton no body).
239
+
240
+ ### Inline edit (commit no submit / Enter)
241
+
242
+ ```tsx
243
+ const columns: DataTableColumnDef<Client>[] = [
244
+ { field: "name", headerName: "Nome", editable: true, sortable: true },
245
+ // ...
246
+ ];
247
+
248
+ <DataTable<Client>
249
+ rows={clients}
250
+ columns={columns}
251
+ onCellEditCommit={async ({ id, field, value, oldValue, row }) => {
252
+ await api.patch(`/clients/${id}`, { [field]: value });
253
+ refreshClients();
254
+ }}
255
+ />;
256
+ ```
257
+
258
+ Double-click numa cell `editable` → input inline; Enter commita; Esc cancela; loading bloqueia outras edições.
259
+
260
+ ### Mobile auto-switch para card
261
+
262
+ Por default, viewports `< 768px` rendem cada row como `<TableCardRow>` no lugar de `<TableRow>`. O toolbar (search/filter/sort) e o footer (paginação) continuam intactos — só o body que troca.
263
+
264
+ ```tsx
265
+ <DataTable<Client>
266
+ rows={clients}
267
+ columns={columns}
268
+ cardBreakpoint={768} // default — abaixo deste pixel, vira card
269
+ // cardBreakpoint={false} // desabilita o auto-switch (mantém table sempre)
270
+ // cardBreakpoint={640} // breakpoint custom
271
+ />
272
+ ```
273
+
274
+ **Mapeamento automático das colunas → card:**
275
+
276
+ - Coluna `isPrimary: true` (ou primeira coluna não-actions) → vai pro **header** do card como título
277
+ - Coluna `type="actions"` → vai pro **headerActions** (canto sup. direito)
278
+ - Checkbox de selection → vai pro header (esquerda do título)
279
+ - Demais colunas visíveis → viram `items` label/value no body do card
280
+
281
+ **Pra eleger qual coluna é o título do card:**
282
+
283
+ ```tsx
284
+ const columns = [
285
+ { field: "id", headerName: "ID", ... },
286
+ { field: "name", headerName: "Nome", isPrimary: true, ... }, // ← vira título do card
287
+ // ...
288
+ ];
289
+ ```
290
+
291
+ **Degradações intencionais no card mode** (silenciosas — não quebram):
292
+
293
+ - Virtualização desligada (renderiza `rowsToRender` integral, paginação ainda limita)
294
+ - Row expansion / Inline editing / Column resize → desativados (sem sentido em card vertical)
295
+ - Group rows → ainda não suportadas (TODO futuro)
296
+
297
+ ### Virtualização (10k+ linhas)
298
+
299
+ ```tsx
300
+ <DataTable<Client>
301
+ rows={tenThousandClients}
302
+ columns={columns}
303
+ virtualize
304
+ estimateRowHeight={56} // opcional — default deriva da density (40/56/64)
305
+ overscan={10} // opcional — rows extras fora da viewport
306
+ paginationConfig={{ enabled: false }} // virtualização geralmente exclui paginação
307
+ />
308
+ ```
309
+
310
+ Usa `@tanstack/react-virtual`. Sticky header e seleção mantêm-se. Performance fica linear até ~100k rows. Requer container com altura definida (`flex-1 min-h-0` ou height fixa).
311
+
312
+ ### Row grouping
313
+
314
+ ```tsx
315
+ <DataTable<Client>
316
+ rows={clients}
317
+ columns={columns}
318
+ groupBy="status" // controlled (string, 1 field na V1) — ou defaultGroupBy uncontrolled
319
+ onGroupByChange={setGroupBy}
320
+ // Default (sem overrides): header column-aligned com chevron + label + count + subtotals.
321
+ // Free-form: passe os 2 overrides abaixo.
322
+ renderGroupHeader={({ group, toggle }) => (
323
+ <span onClick={toggle}>
324
+ {group.label} ({group.count})
325
+ </span>
326
+ )}
327
+ renderGroupContent={({ group }) => <CardsGrid rows={group.rows} />}
328
+ />
329
+ ```
330
+
331
+ Pagination é desligada **automaticamente** quando `groupBy` está ativo. Sem os overrides, o default é column-aligned (mantém grid layout); com `renderGroupHeader`/`renderGroupContent`, o grupo vira free-form (full-width, ideal pra hierarquias complexas). Referência: `src/preview/pages/ClientsGroupedPreview.tsx` (2 modos).
332
+
333
+ ### Row expansion (painel detalhe)
334
+
335
+ ```tsx
336
+ const columns = [
337
+ { field: "id", headerName: "ID", expandable: true }, // ← chevron + click trigger
338
+ // ...
339
+ ];
340
+
341
+ <DataTable<Client>
342
+ rows={clients}
343
+ columns={columns}
344
+ renderRowExpansion={({ row }) => <ClientDetailPanel client={row} />}
345
+ singleExpand // opcional — default false (múltiplas rows abertas)
346
+ />;
347
+ ```
348
+
349
+ O chevron aparece na coluna marcada `expandable: true`. Controlled opcional via `expandedRowIds` + `onExpandedRowIdsChange` (ou `defaultExpandedRowIds` uncontrolled). Mutuamente exclusivo com `groupBy` (groupBy tem precedência). Referência: `src/preview/pages/ClientsExpandablePreview.tsx`.
350
+
351
+ ### Tree-data (hierarquia multi-nível)
352
+
353
+ Hierarquia tipo AG Grid: cada linha continua **FLAT** em `rows` e o **caminho** (`getTreeDataPath`) define a árvore. O DataTable reconstrói os níveis a partir dos caminhos e renderiza indentação + chevron na coluna primária.
354
+
355
+ ```tsx
356
+ const columns = [
357
+ { field: "name", headerName: "Licenciado", treeColumn: true }, // ← coluna primária da árvore
358
+ // ...
359
+ ];
360
+
361
+ // O path sobe a cadeia de patrocinador até a raiz: ["L-001", "L-010", "L-100"]
362
+ const byId = new Map(rows.map((r) => [r.id, r]));
363
+ const getTreeDataPath = (row: NetworkRow): string[] => {
364
+ const path: string[] = [];
365
+ let cur: NetworkRow | undefined = row;
366
+ while (cur) {
367
+ path.unshift(cur.id);
368
+ cur = cur.parentId ? byId.get(cur.parentId) : undefined;
369
+ }
370
+ return path;
371
+ };
372
+
373
+ <DataTable<NetworkRow>
374
+ rows={rows} // ← FLAT (não aninhadas)
375
+ columns={columns}
376
+ getRowId={(r) => r.id}
377
+ getTreeDataPath={getTreeDataPath}
378
+ treeData={{
379
+ defaultExpanded: true, // árvore começa aberta (default true)
380
+ showDescendantCount: true, // mostra "(N)" descendentes ao lado do nome
381
+ }}
382
+ />;
383
+ ```
384
+
385
+ Regras:
386
+
387
+ - `getTreeDataPath(row)` retorna o array do caminho (`[raiz, ..., self]`). Linhas com path vazio são ignoradas da árvore.
388
+ - Se **nenhuma** coluna marcar `treeColumn: true`, o DataTable usa a primeira coluna não-`actions`.
389
+ - Estado de expansão reusa a máquina de row-expansion (`expandedRowIds` / `defaultExpandedRowIds` / `onExpandedRowIdsChange`). O Set guarda os ids que **divergem** do `defaultExpanded`.
390
+ - **Pagination desliga automaticamente** (paginar cortaria ramos). Não suportado em server mode — passe todas as rows do escopo + `virtualize` se necessário.
391
+ - Precedência quando mais de um modo é passado: `groupBy` > `getTreeDataPath` > `renderRowExpansion`.
392
+ - Search/sort operam sobre as rows; a árvore é reconstruída do resultado.
393
+ - **Expand-all / collapse-all** programático via imperative ref: `ref.current.expandAllTree()` / `ref.current.collapseAllTree()` (ver seção [Imperative ref](#imperative-ref)). No-op fora do modo tree-data. Respeita `treeData.defaultExpanded` e opera sobre todas as rows pós-filtro/sort. O DS não embute botões na toolbar — o consumer fia os botões e chama via ref.
394
+
395
+ ```tsx
396
+ const tableRef = useRef<DataTableRef>(null);
397
+
398
+ <button onClick={() => tableRef.current?.expandAllTree()}>Expandir tudo</button>
399
+ <button onClick={() => tableRef.current?.collapseAllTree()}>Recolher tudo</button>
400
+
401
+ <DataTable<NetworkRow> ref={tableRef} getTreeDataPath={getTreeDataPath} /* ... */ />
402
+ ```
403
+
404
+ Referência: `src/preview/pages/ClientsTreePreview.tsx`.
405
+
406
+ ### Polish de célula — Read-more (Ler mais)
407
+
408
+ Trunca conteúdo longo e abre o texto completo num popover ao clicar em "Ler mais". Equivalente DS do `ReadMoreCell` legado (que usava tooltip).
409
+
410
+ ```tsx
411
+ const columns = [
412
+ // 1 linha + reticências + gatilho "Ler mais" (default)
413
+ { field: "obs", headerName: "Observação", readMore: true },
414
+ // N linhas antes de truncar + label custom
415
+ {
416
+ field: "bio",
417
+ headerName: "Bio",
418
+ readMore: { lines: 2, label: "Ver tudo" },
419
+ },
420
+ ];
421
+ ```
422
+
423
+ - `readMore: true` → 1 linha, label "Ler mais". `readMore: { lines?, label? }` customiza.
424
+ - Desativa o `ellipsis` da cell automaticamente (a add-on gerencia o próprio truncate).
425
+ - Aplica-se ao render default **ou** ao `render` custom (o nó é envolvido). Ignorado em `type: "actions"`, células em edição e na coluna primária de tree-data.
426
+ - O texto do popover deriva do `valueFormatter`/`formatValue`/value (string) — pra HTML rico, passe um `render` que retorna o nó; ele é exibido no popover.
427
+
428
+ ### Polish de célula — Copy (copiar valor)
429
+
430
+ Ícone de copiar revelado no hover/foco da célula, com feedback "Copiado!" por ~2s. Usa `navigator.clipboard` — **sem dependência nova**.
431
+
432
+ ```tsx
433
+ const columns = [
434
+ // copia o texto renderizado da célula
435
+ { field: "email", headerName: "E-mail", copyable: true },
436
+ // copia um valor derivado da row (ex: id puro) + aria-label custom
437
+ {
438
+ field: "doc",
439
+ headerName: "CPF",
440
+ copyable: { value: (row) => row.cpfRaw, label: "Copiar CPF" },
441
+ },
442
+ ];
443
+ ```
444
+
445
+ - `copyable: true` → copia o texto da célula. `copyable: { value?, label? }` customiza: `value` aceita string ou `(row) => string`; `label` é o aria-label/title do botão.
446
+ - O ícone só aparece no hover/foco (não polui a célula). O click não dispara `onRowClick`/seleção.
447
+ - `readMore` tem precedência: se ambos forem definidos na mesma coluna, vale `readMore`.
448
+
449
+ ### Grab-to-scroll horizontal
450
+
451
+ Arrastar o corpo da tabela (mouse/pen) pra rolar lateralmente — equivalente ao `useGrabToScroll` legado.
452
+
453
+ ```tsx
454
+ {/* nativo — nada a fazer; pra desligar: */}
455
+ <DataTable<Client> rows={clients} columns={columns} grabToScroll={false} />
456
+ ```
457
+
458
+ - Prop raiz `grabToScroll: boolean` — **nativo, default `true`** (todas as tabelas já vêm com ele; passe `false` pra desabilitar).
459
+ - Um arrasto só inicia após ~6px de movimento → clique/seleção de célula preservados; o clique pós-arrasto é suprimido.
460
+ - **Scroll por roda do mouse permanece intacto.** Pulado em touch (scroll nativo já funciona) e em alvos interativos (botões, inputs, células editáveis/expansíveis/de seleção/ações).
461
+ - **O cursor `grab` só aparece quando há overflow** — tabela que cabe na tela não mostra a mãozinha, e volta a mostrar se uma coluna crescer, a view trocar ou o container encolher (re-medido por `ResizeObserver`). Até 2026-08-21 a mãozinha era incondicional e prometia um gesto que o handler recusava: se você viu isso num projeto, é versão anterior.
462
+
463
+ ### Tela cheia (fullscreen)
464
+
465
+ Toggle ⤢ na toolbar expande a DataTable pra ocupar a viewport inteira; segundo clique ou **Esc** volta.
466
+
467
+ ```tsx
468
+ <DataTable<Client>
469
+ rows={clients}
470
+ columns={columns}
471
+ toolbar={{ enableFullscreen: true }}
472
+ />
473
+ ```
474
+
475
+ - `toolbar.enableFullscreen: true` (default `false`) renderiza o tool button entre Filtros e Configurações.
476
+ - O container raiz vira overlay `fixed inset-0` (z-index `--z-index-modal`) com bg do canvas. Estado interno uncontrolled.
477
+
478
+ ### View Kanban (table ⇄ board)
479
+
480
+ ```tsx
481
+ <DataTable<Client>
482
+ rows={clients}
483
+ columns={columns}
484
+ defaultViewMode="kanban" // uncontrolled — ou viewMode + onViewModeChange (controlled)
485
+ kanbanConfig={{
486
+ groupByField: "status", // valor do field define a coluna do board
487
+ renderCard: ({ row }) => ({
488
+ // slots do card — id/columnId são derivados automaticamente
489
+ title: row.name,
490
+ subtitle: row.email,
491
+ value: formatBRL(row.value),
492
+ }),
493
+ enableDnD: true,
494
+ onCardMove: (cardId, from, to) => patchStatus(cardId, to),
495
+ }}
496
+ />
497
+ ```
498
+
499
+ Quando `viewMode`/`defaultViewMode` + `kanbanConfig` estão definidos, a toolbar auto-renderiza o segmented table/kanban (override/esconda via `toolbar.viewToggle`). Filter/search/sort/selection continuam aplicados às rows; paginação, density toggle e columns popover são desligados automaticamente no board. `kanbanConfig.columns` (opcional, `KanbanColumn[]`) fixa ordem/label/dotColor das colunas — sem ele, as colunas derivam dos valores únicos de `groupByField`. Outras opções do `kanbanConfig`: `renderCardContent` (override total do miolo do card), `getCardMenuItems`/`getColumnMenuItems` (menus "⋯"), `onAddCard`/`onAddInFooter`, `emptyLabel`/`addLabel`.
500
+
501
+ ### View Lista (table ⇄ list)
502
+
503
+ ```tsx
504
+ <DataTable<Client>
505
+ rows={clients}
506
+ columns={columns}
507
+ viewMode={viewMode} // "table" | "list" | "kanban"
508
+ onViewModeChange={setViewMode} // ou defaultViewMode (uncontrolled)
509
+ listConfig={{
510
+ renderItem: (row, { depth }) => (
511
+ <div className="flex w-full items-center gap-gp-lg">
512
+ <Avatar size="md" colorHex={row.avatarColor}>{row.initials}</Avatar>
513
+ <div className="flex min-w-0 flex-1 flex-col">
514
+ <span className="truncate text-body-md font-semibold">{row.name}</span>
515
+ <span className="truncate text-caption-md text-fg-muted">{row.email}</span>
516
+ </div>
517
+ <Chip color="success" variant="soft" size="sm" shape="pill">Ativo</Chip>
518
+ </div>
519
+ ),
520
+ // hierarchical: true, // + getTreeDataPath → lista em ÁRVORE (conectores)
521
+ // getMenuItems: (row) => [...], // menu "⋯" por item
522
+ }}
523
+ />
524
+ ```
525
+
526
+ `listConfig` habilita a 3ª view: o toggle vira **Tabela / Lista** (+ Kanban se `kanbanConfig`). O DataTable mantém a **MESMA toolbar** (busca/filtros/views/ações/totalizadores) e só troca o corpo por um `<List>` do DS, alimentado pelas rows processadas (`filter+search+sort`; por padrão **sem paginação** — mostra todas, igual ao kanban). Passe `listConfig.paginated: true` pra **paginar a lista flat** (usa a mesma paginação da tabela + mostra o footer; ignorado em `hierarchical`). `listConfig.renderItem(row, { depth, open })` desenha o card de cada item. Com `hierarchical: true`, a lista aninha em árvore com indentação/conectores e `depth` por nível, usando `listConfig.getPath` (caminho raiz→self) — ou, se ausente, o `getTreeDataPath` do DataTable. **Use `listConfig.getPath` quando quiser tabela FLAT (paginada) + lista em ÁRVORE** no mesmo DataTable (o `getTreeDataPath` ligaria o tree-data na tabela e desligaria a paginação). Showcase: `#/clients-list-view`.
527
+
528
+ ### Saved views
529
+
530
+ ```tsx
531
+ import { savedViewsMockService } from "@/components/ui/DataTable";
532
+
533
+ <DataTable<Client>
534
+ rows={clients}
535
+ columns={columns}
536
+ savedViewsService={savedViewsMockService} // troque pelo seu service em prod
537
+ />;
538
+ ```
539
+
540
+ Service contract em `services/saved-views.types.ts` — `list / save / delete` (todos recebem `persistId` como primeiro arg). Persiste o `DataTableSavedViewState` (filterModel, sortModel, density, layout de colunas, viewMode, groupBy, expandedRowIds) como JSON — `search` e paginação são voláteis e NÃO entram na view.
541
+
542
+ **`allowCreateView` (v0.23.0)** — `allowCreateView={false}` esconde o botão "+" das visões (exibe SÓ os `defaultViews` + Default, read-only; o usuário não cria/salva visões). Default `true`.
543
+
544
+ **`maxViewTabs`** — quantas abas de visão cabem na barra, **contando a "Default"**. Default `3`, ou seja **2 presets** de `defaultViews` viram aba.
545
+
546
+ ⚠️ O excedente é **cortado** (`.slice()` no `TableToolbarViews`): com 3 presets, o terceiro não aparece — sem erro e sem overflow. Em **DEV** sai um `console.warn` (v0.43.1) nomeando o que foi cortado e o `maxViewTabs` que resolve; em produção o corte é silencioso. Precisa de N abas fixas? `maxViewTabs={N + 1}`. Lembre que a aba **Default já é a visão sem filtro** — preset "Todos"/"Todas" duplica ela e gasta um slot.
547
+
548
+ **viewMode "sticky" ao trocar de visão (v0.23.0)** — aplicar uma visão (preset/Default) só troca o `viewMode` se a visão **definir um explicitamente** (ex.: preset salvo em Lista/Kanban). Presets sem `viewMode` (o caso comum) **mantêm** o que o usuário está vendo — alternar de visão não flipa Tabela↔Lista↔Kanban. Pra um preset abrir numa view específica, passe `viewMode` no `presetView({ ... })`.
549
+
550
+ ### Tipo de coluna custom (registry)
551
+
552
+ ```tsx
553
+ const RatingColumnType: ColumnTypeDefinition = {
554
+ type: "rating",
555
+ // operators: array de { id, label } — id usa nomes canônicos do FilterOperator
556
+ // (equals, neq, contains, notContains, startsWith, endsWith, gt, lt, gte, lte,
557
+ // isAnyOf, isNoneOf, between, isEmpty, isNotEmpty). Label aparece no dropdown
558
+ // de operadores do popover Filtros + chip toolbar (via DEFAULT_OP_LABELS).
559
+ operators: [
560
+ { id: "equals", label: "é" },
561
+ { id: "gt", label: "maior que" },
562
+ { id: "lt", label: "menor que" },
563
+ ],
564
+ renderCell: ({ value }) => <Stars n={Number(value) || 0} />,
565
+ renderFilterInput: ({ value, onChange }) =>
566
+ <NumberInput min={0} max={5} value={value} onChange={onChange} />,
567
+ // obrigatório (sem `?` no type) — input do popover do chip rápido.
568
+ // Pode reusar o mesmo widget do renderFilterInput.
569
+ renderFastFilterInput: ({ value, onChange }) =>
570
+ <NumberInput min={0} max={5} value={value} onChange={onChange} />,
571
+ matchesFilter: (cellValue, filterValue, operator) => {
572
+ const n = Number(cellValue) || 0;
573
+ const f = Number(filterValue) || 0;
574
+ if (operator === "equals") return n === f;
575
+ if (operator === "gt") return n > f;
576
+ if (operator === "lt") return n < f;
577
+ return null;
578
+ },
579
+ };
580
+
581
+ // Registro feito uma vez na boot:
582
+ columnTypeRegistry.register(RatingColumnType);
583
+
584
+ // Uso:
585
+ { field: "rating", headerName: "Rating", type: "rating" as any }
586
+ ```
587
+
588
+ ---
589
+
590
+ ## Filtros — funil (drawer simple) + avançado (Configurações)
591
+
592
+ A toolbar separa dois níveis de filtro, sempre disponíveis (gated por
593
+ `toolbar.enableFilters !== false` + colunas filtráveis):
594
+
595
+ - **Funil** → abre um **drawer lateral** com TODOS os filtros em form vertical.
596
+ Aplicação LIVE, operator inferido do `filterType` (multiSelect → isAnyOf,
597
+ text → contains, date → between, etc). O caminho simples pro user típico.
598
+ - **Configurações → Filtros avançados** → query builder completo: modo **Visual**
599
+ (AND/OR + operadores explícitos + Adicionar condição) e modo **Avançado** (SQL-like,
600
+ round-trip-safe pra todos os operadores).
601
+
602
+ A prop `simpleFilter` é **opcional** — só customiza o drawer do funil:
603
+
604
+ ```tsx
605
+ {/* Funil + avançado vêm de graça — sem configuração */}
606
+ <DataTable rows={...} columns={...} />
607
+
608
+ {/* Customizar o drawer do funil */}
609
+ <DataTable
610
+ simpleFilter={{
611
+ hiddenFields: ["internal"], // não mostra no drawer (só no avançado)
612
+ title: "Refinar busca",
613
+ size: "lg", // 560px (default md = 400px)
614
+ }}
615
+ />
616
+ ```
617
+
618
+ ### `simpleFilter` (opcional)
619
+
620
+ | Prop | Tipo | Default | Quando usar |
621
+ | --------------------------- | ------------------------------ | -------------- | -------------------------------------------------------------------------------------- |
622
+ | `simpleFilter.hiddenFields` | `string[]` | `[]` | Fields que NÃO aparecem no drawer do funil (só no avançado). Útil pra filtros técnicos |
623
+ | `simpleFilter.title` | `string` | `"Filtros"` | Override do título do header do drawer |
624
+ | `simpleFilter.size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` (400px) | Use `"lg"` (560px) se há muitos campos com widgets largos (dates/multi-select) |
625
+
626
+ ---
627
+
628
+ ## ⚠️ filterModel controlado — operator correto por filterType
629
+
630
+ **Quando passar `filterModel` como prop controlada (em vez de uncontrolled), use o operator
631
+ correto pro `filterType` da coluna.** Operator errado = popover Filtros mostra o Select de
632
+ operador **vazio** porque o operator não está nos `operators` do column-type.
633
+
634
+ | `filterType` da coluna | Operators válidos | Default sugerido |
635
+ | ---------------------- | --------------------------------------------------------------------------------------------- | ---------------- |
636
+ | `multiSelect` | `isAnyOf`, `isNoneOf`, `isEmpty`, `isNotEmpty` | `isAnyOf` |
637
+ | `select` | `equals`, `neq`, `isEmpty`, `isNotEmpty` | `equals` |
638
+ | `text` (default) | `contains`, `notContains`, `equals`, `neq`, `startsWith`, `endsWith`, `isEmpty`, `isNotEmpty` | `contains` |
639
+ | `number` | `equals`, `neq`, `gt`, `lt`, `gte`, `lte` | `equals` |
640
+ | `date` | `between`, `equals`, `gt`, `lt`, `gte`, `lte` | `between` |
641
+ | `boolean` | `equals` | `equals` |
642
+
643
+ ```tsx
644
+ // ❌ ERRADO — Status é multiSelect mas operator é "equals"
645
+ // → Popover Filtros mostra Select operador VAZIO
646
+ const INITIAL_FILTERS: FilterModel = {
647
+ items: [{ id: "f1", field: "statusId", operator: "equals", value: "active" }],
648
+ logicOperator: "AND",
649
+ };
650
+
651
+ // ✅ CORRETO — operator bate com filterType=multiSelect
652
+ const INITIAL_FILTERS: FilterModel = {
653
+ items: [
654
+ { id: "f1", field: "statusId", operator: "isAnyOf", value: "active" },
655
+ ],
656
+ logicOperator: "AND",
657
+ };
658
+ ```
659
+
660
+ **Defesa em profundidade:** `FilterRowEditor` detecta operator inválido e faz fallback pro
661
+ primeiro operator do column-type + auto-normaliza via `onChange`. Mas é melhor declarar
662
+ correto desde o início.
663
+
664
+ **Atalho pra presets uncontrolled:** se você usar `defaultViews={[presetView({...})]}` em
665
+ vez de filterModel controlled, o controller normaliza automaticamente via
666
+ `normalizeFilterModelForColumns` na hidratação. Só recomendado se você não precisa de
667
+ controle externo do filterModel.
668
+
669
+ ---
670
+
671
+ ## Imperative ref
672
+
673
+ ```tsx
674
+ const tableRef = useRef<DataTableRef>(null);
675
+
676
+ <DataTable ref={tableRef} ... />
677
+
678
+ tableRef.current?.getSelectedIds(); // (string | number)[]
679
+ tableRef.current?.getSelectedCount(); // number
680
+ tableRef.current?.clearSelection();
681
+ tableRef.current?.getState(); // DataTableState snapshot
682
+ tableRef.current?.refresh(); // server mode: re-disparar fetchData
683
+ tableRef.current?.exportCsv("filtered"); // download CSV — escopo "all" | "filtered" | "selected"
684
+ tableRef.current?.resetPersistedState(); // limpa o localStorage (no-op sem persistId)
685
+ tableRef.current?.expandAllTree(); // tree-data: expande todos os nós (no-op fora de tree-data)
686
+ tableRef.current?.collapseAllTree(); // tree-data: recolhe todos os nós (no-op fora de tree-data)
687
+ ```
688
+
689
+ `expandAllTree` / `collapseAllTree` só fazem efeito em modo tree-data (`getTreeDataPath`). Operam sobre todas as rows pós-filtro/sort (tree-data desliga paginação) via `collectExpandableTreeIds` e respeitam `treeData.defaultExpanded` — escrevem o Set de divergência correto (`[]` ou todos os ids expansíveis).
690
+
691
+ ---
692
+
693
+ ## Configs detalhadas
694
+
695
+ ### `toolbar` (DataTableToolbarConfig)
696
+
697
+ - `title?` — string no canto esquerdo
698
+ - `enableSearch?` (true) — ToolbarSearch slot
699
+ - `enableRefresh?` (true) — botão Refresh após o search (server mode refetch; client mode spinner)
700
+ - `enableFilters?` (true) — controle de filtros (só aparece se ao menos uma coluna tem `enableColumnFilter`)
701
+ - `enableColumns?` (true) — ColsPopover (show/hide, pin, reorder via drag)
702
+ - `enableDensity?` (true) — ToolbarSegmented compact/standard/comfortable
703
+ - `enableExport?` (false) — `true` = dropdown Exportar com CSV default; objeto `{ formats?, items? }` pra formatos custom
704
+ - `enableFullscreen?` (false) — botão ⤢ na toolbar (entre Filtros e Configurações) que expande a tabela pra viewport inteira; Esc volta
705
+ - `moreMenu?` — `{ items: DataTableMoreMenuItem[] }` — MoreMenu (⋯) no canto direito
706
+ - `customLeft?` — ReactNode livre após search/refresh (controls custom)
707
+ - `viewToggle?` — override/esconde o segmented table/kanban auto-renderizado
708
+
709
+ > Bulk actions vão em `selectionConfig.actions` (não no toolbar). Preset views vão na prop
710
+ > `defaultViews` da DataTable (não no toolbar).
711
+
712
+ ### `paginationConfig`
713
+
714
+ - `enabled` (true)
715
+ - `initialPageSize` (25)
716
+ - `pageSizeOptions` ([10, 25, 50, 100])
717
+
718
+ > O modo client/server **não é prop** — é derivado automaticamente de `rows` vs `fetchData`.
719
+
720
+ ### `selectionConfig`
721
+
722
+ - `enabled` (false)
723
+ - `enableGlobal` (false) — "selecionar todos" com modo include/exclude
724
+ - `actions?: (selectedIds: GridRowId[], clearSelection: () => void) => ReactNode` — render-prop chamada dentro do BulkActionsBar (NÃO é ReactNode direto)
725
+
726
+ ### `getRowId?: (row: T) => GridRowId` (prop raiz)
727
+
728
+ Extrai o id da row — default `row.id`. É prop top-level da DataTable, **não** vai dentro de `selectionConfig`.
729
+
730
+ ### `densityItems?: ToolbarSegmentedItem<TableDensity>[]`
731
+
732
+ Customiza os 3 botões do segmented. Default: compact / standard / comfortable.
733
+
734
+ ### `cardBreakpoint?: number | false`
735
+
736
+ - `number` (default `768`) — viewport `< N px` ativa o card mode (rows viram `<TableCardRow>`)
737
+ - `false` — desabilita o auto-switch (mantém table view em qualquer viewport)
738
+
739
+ Use `false` em telas onde o card mode não faz sentido (ex: tabela dentro de modal pequeno que já é mobile-friendly de outra forma).
740
+
741
+ ### `autoFit?: boolean` (default `true`)
742
+
743
+ Auto-distribui as colunas para ocupar todo o container, em 3 camadas:
744
+
745
+ 1. **Type Heuristics** — cada `column.type` tem `defaultWidth` do registry. Se a coluna define `width`, esse vira a **base/mínimo** da coluna (ver Flex Distribution).
746
+ 2. **Smart Content Sampling** — mede o texto do header + primeiras 20 rows via canvas (`measureText`) e ajusta width pra caber o conteúdo. Respeita `col.minWidth` e `col.maxWidth`.
747
+ 3. **Flex Distribution (proporcional)** — sobrando espaço no container, distribui **proporcionalmente** entre as colunas (peso = largura-base de cada uma), como uma tabela flex faz naturalmente. Colunas pequenas crescem pouco, largas crescem mais — sem "coluna gigante" puxando 100% do espaço. Funciona pra qualquer nº de colunas.
748
+
749
+ **Header nunca trunca (`...`):** toda coluna tem como piso a largura necessária pra mostrar o `headerName` inteiro (texto + ícone de tipo + reserva de sort/menu). Isso vale **inclusive** pra colunas com `width` explícito menor que o header — a width do consumer não pode esconder o título (só `maxWidth` menor que o header trunca, e aí é decisão explícita do consumer).
750
+
751
+ **`col.width` é base, não trava fixa (v0.22.0+):** colunas com `width` explícito entram na distribuição proporcional usando a width como piso (crescem pra preencher, nunca encolhem abaixo dela). Antes a width era 100% fixa, o que jogava todo o espaço sobrando na única coluna sem width (virava "coluna gigante"). Pra travar uma coluna de fato, use `width` + `maxWidth` iguais (ou um `type` fixo como `actions`/`checkbox`, que ficam fora do flex). Se **todas** as colunas têm `width` explícito, o layout fixo do consumer é respeitado e o espaço sobrando fica vazio à direita.
752
+
753
+ Observado via `ResizeObserver` no container — recalcula quando viewport muda. Re-mede e re-aplica de forma consistente ao alternar **Tabela ↔ Lista** (o corpo da tabela desmonta na view Lista; ao voltar, o autoFit reata o observer no node novo — mesma distribuição da 1ª carga).
754
+
755
+ **Precedência de width:** resize manual (drag pelo user) > autoFit > `col.width` > `typeDef.defaultWidth`.
756
+
757
+ **Para desligar:** `autoFit={false}` mantém comportamento legacy (cada coluna usa `col.width` ou default fixo; espaço sobrando vira vazio à direita). Resize manual continua disponível em ambos os modos.
758
+
759
+ ```tsx
760
+ // Default — fluid automático
761
+ <DataTable rows={rows} columns={cols} />
762
+
763
+ // Opt-out
764
+ <DataTable rows={rows} columns={cols} autoFit={false} />
765
+
766
+ // width = BASE/mínimo (cresce proporcional p/ preencher). Pra TRAVAR de fato,
767
+ // use width + maxWidth iguais, ou um type fixo (actions/checkbox).
768
+ const cols = [
769
+ { field: "id", width: 80, maxWidth: 80 }, // travada em 80px
770
+ { field: "code", width: 120 }, // base 120, cresce no flex
771
+ { field: "name" }, // sem width — flui pelo autoFit
772
+ { field: "actions", type: "actions", getActions }, // fora do flex; largura pelo nº de ações
773
+ ];
774
+ ```
775
+
776
+ > Nota: o melhor padrão é **não** setar `width` nas colunas de dados e deixar o autoFit
777
+ > distribuir (as skills `crud-builder`/`list-builder` geram assim). Setar `width` em
778
+ > todas as colunas trava o layout e deixa espaço vazio à direita.
779
+
780
+ ### `grabToScroll?: boolean` (**nativo — default `true`**)
781
+
782
+ Grab-to-scroll horizontal: arrastar o corpo da tabela (mouse/pen) rola lateralmente. **Já vem ligado em todas as tabelas** — não precisa configurar. Threshold de ~6px separa arrasto de clique (seleção/click de célula preservados; o clique pós-arrasto é suprimido). Scroll por roda intacto; pulado em touch e alvos interativos. Passe `grabToScroll={false}` só se quiser desabilitar.
783
+
784
+ ```tsx
785
+ {/* nativo — nada a fazer. Pra desligar: */}
786
+ <DataTable rows={rows} columns={cols} grabToScroll={false} />
787
+ ```
788
+
789
+ ### `persistId?: string` (workspace "Default" persistente — schema v4)
790
+
791
+ Quando definido, **todo** o workspace "Default" é salvo em localStorage:
792
+
793
+ - `density`, `sortModel`, `pageSize`, `currentPage`
794
+ - `columnWidths` (resize manual), `pinnedColumns`, `hiddenColumns`, `columnOrder`
795
+ - `filterModel`, `search` (texto debounced)
796
+ - `viewMode`, `groupBy`, `expandedRowIds`
797
+ - `lastActiveViewId` — qual view estava aplicada no último uso
798
+
799
+ **Como views custom interagem com Default:**
800
+
801
+ - User filtra/busca/etc → snapshot da Default é atualizado em tempo real
802
+ - User aplica view custom (preset ou saved) → snapshot da Default fica **congelado** (não polui)
803
+ - User volta para Default → `applyDefault` restaura tudo (filter, search, page, etc) do snapshot intacto
804
+ - User precisa **limpar manualmente** (clear search input, remover filtros via UI) para resetar
805
+
806
+ **Reset programático:**
807
+
808
+ ```ts
809
+ ref.current?.resetPersistedState(); // remove entry inteira do localStorage
810
+ ```
811
+
812
+ **Schema versionado:** entries antigos (v3 ou menor) são descartados silenciosamente — DataTable cai no comportamento default sem erro. Schema atual `v4`.
813
+
814
+ ---
815
+
816
+ ## Performance
817
+
818
+ - `columns` **deve** ser memoizado com `useMemo` no pai
819
+ - O processor (filter → search → sort → paginate) usa useMemo cascateado — mudar só de página NÃO re-roda filter/search/sort
820
+ - Provider value é memoizado — re-render do pai não dispara cascade em rows
821
+ - Use `virtualize` para > ~500 rows visíveis (ou desde sempre se UX permite)
822
+ - Saved views + persistId: ambos consomem o mesmo `DataTableState`, podem coexistir
823
+
824
+ ---
825
+
826
+ ## ARIA
827
+
828
+ Tudo proveniente do `<Table>` primitive: `role="grid"`, `role="row"`, `role="columnheader"` com `aria-sort`, `role="gridcell"`. Keyboard navigation segue WAI-ARIA grid pattern. Bulk bar tem `role="region"` com aria-label.
829
+
830
+ ---
831
+
832
+ ## Troubleshooting
833
+
834
+ | Sintoma | Causa provável | Fix |
835
+ | ---------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------- |
836
+ | Tabela re-renderiza inteira a cada digit | `columns` não memoizado | `useMemo(() => [...], [deps])` |
837
+ | Filter chip não aparece | Coluna sem `enableColumnFilter: true` | Adicionar flag |
838
+ | Filter popover vazio | Nenhuma coluna com `enableColumnFilter` | Adicionar a pelo menos 1 coluna |
839
+ | Sort não funciona | Coluna sem `sortable: true` | Adicionar flag |
840
+ | Density não persiste | `persistId` ausente | Adicionar `persistId="meu-table"` |
841
+ | Server mode loop infinito | `fetchData` não memoizado | `useCallback(fetchData, [deps])` |
842
+ | Virtualização "pula" | `estimateRowHeight` muito diferente do real | Ajustar pro height médio observado |
843
+ | Inline edit não salva | `onCellEditCommit` retorna sem await | Retornar Promise; controller aguarda |
844
+ | Saved views não persiste | `savedViewsMockService` em prod | Implementar `SavedViewsService` real |
845
+ | Group header sem totalizer | Coluna sem `aggregate` declarado | Definir `aggregate: "sum" \| "avg" \| "count" \| "min" \| "max" \| fn` na coluna |
846
+ | Coluna actions com filter chip | `type: "actions"` deveria desabilitar filter | Reportar — esse type bloqueia sort/filter por design |
847
+
848
+ ---
849
+
850
+ ## Padrões internos (referência rápida)
851
+
852
+ - **God component evitado** — DataTable orquestra; lógica pesada em hooks (`use-filter-popover-adapter`, `use-sort-popover-adapter`, `use-cols-popover-adapter`, `use-data-table-processor`, etc)
853
+ - **Vocabulário único de operador** — ids longos do `FilterModel` (`equals`, `neq`, `gt`, `gte`…) em todo o fluxo (popover, parser SQL, chips, adapter). Sem tradução curto↔longo. Label do chip vem do registry do column-type (`opLabel`), com `DEFAULT_OP_LABELS` como fallback
854
+ - **Value resolution shared** — `utils/resolve-value.ts` (`getFieldValue / applyValueGetter / applyFormatter`) usado por processor, group-rows e cell render
855
+ - **Column types via registry** — `column-types/column-type-registry.ts`; `console.warn` em duplicate (não throw, suporta hot reload)
856
+ - **Row variants discriminadas** — `groupRow / groupContentRow / expansionRow / dataRow` via Symbol-as-discriminator (type-safe)
857
+ - **Sortable head cell renomeado** — `DataTableSortableHeadCell` (consistência com prefixo)
858
+
859
+ ---
860
+
861
+ ## V2 (planejado, não V1)
862
+
863
+ - Extração final do `<DataTableBody>` para componente próprio
864
+ - Hooks dedicados pra inline-edit, keyboard-nav, grouping, expansion (atualmente inline no orquestrador)
865
+ - Migration helper auto-aplicado para saved views quando colunas removidas
866
+ - Unit tests cobertura ≥ 80% nos hooks críticos (`use-column-resize`, `group-rows`, `expand-rows`, `use-data-table-processor`)
867
+ - Mobile parts da toolbar (`ToolbarMobileDialog` etc) decidir mantém ou remove