@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,84 @@
1
+ # DatePicker — USAGE
2
+
3
+ **Categoria:** composto (Popover + Calendar). Seletor de data(s) com trigger no estilo input do DS.
4
+
5
+ ## Quando usar
6
+ - Campo de formulário pra escolher **uma** data (nascimento, vencimento, data de referência).
7
+ - Selecionar um **intervalo** de datas (período de relatório, filtro por data inicial/final).
8
+ - Selecionar **várias datas soltas** (não contíguas).
9
+
10
+ ## Import
11
+ ```tsx
12
+ import { DatePicker } from "@/components/ui/DatePicker";
13
+ import type { DatePickerProps, DateRange } from "@/components/ui/DatePicker";
14
+ ```
15
+
16
+ ## Variants (`mode`) — muda o shape do `value`
17
+
18
+ `DatePickerProps` é uma discriminated union por `mode`. Trocar de modo troca o TIPO de
19
+ `value`/`onValueChange` junto — trate como union, não como prop solta.
20
+
21
+ | `mode` | Obrigatória? | `value` | `onValueChange` | Nº de meses (default) |
22
+ |---|---|---|---|---|
23
+ | `"single"` (default) | não (`mode?: "single"`) | `Date \| undefined` | `(value: Date \| undefined) => void` | 1 |
24
+ | `"range"` | sim (`mode: "range"`) | `DateRange \| undefined` (`{ from: Date \| undefined; to?: Date }`, reexportado de `react-day-picker`) | `(value: DateRange \| undefined) => void` | 2 |
25
+ | `"multiple"` | sim (`mode: "multiple"`) | `Date[] \| undefined` | `(value: Date[] \| undefined) => void` | 1 |
26
+
27
+ Comportamento de fechamento do popover por modo:
28
+ - `single`: fecha ao clicar numa data.
29
+ - `range`: só fecha quando `from` **e** `to` estão preenchidos (clique único no primeiro dia não fecha).
30
+ - `multiple`: **não fecha sozinho** — o usuário fecha clicando fora.
31
+
32
+ Label do trigger por modo (não é customizável — ver Gotchas). Formatos conferidos no
33
+ showcase, não inferidos do código:
34
+ - `single`: mês por extenso — `19 de junho de 2026` (`month: "long"`).
35
+ - `range`: mês abreviado nas duas pontas — `10 de jul. de 2026 – 18 de ago. de 2026`
36
+ (`month: "short"`); com só o `from` preenchido, mostra apenas ele.
37
+ - `multiple`: contagem — `1 data selecionada` / `N datas selecionadas`.
38
+
39
+ ## Props essenciais
40
+ | Prop | Tipo | Default | Descrição |
41
+ |---|---|---|---|
42
+ | `mode` | `"single" \| "range" \| "multiple"` | `"single"` | Modo de seleção — ver tabela acima pro shape de `value`. |
43
+ | `value` | `Date` \| `DateRange` \| `Date[]` (conforme `mode`) | — | Seleção controlada. |
44
+ | `onValueChange` | conforme `mode` (ver tabela acima) | — | Callback de mudança. |
45
+ | `placeholder` | `string` | `"Selecione a data"` (single/multiple) ou `"Selecione o período"` (range) | Texto do trigger quando nada está selecionado. |
46
+ | `disabled` | `boolean` | — | Desabilita o **trigger** (botão inteiro). |
47
+ | `align` | `"start" \| "center" \| "end"` | `"start"` | Alinhamento do `PopoverContent`. |
48
+ | `numberOfMonths` | `number` | `1` (single/multiple) / `2` (range) | Nº de meses exibidos no `Calendar` interno. |
49
+ | `className` | `string` | — | className do trigger (mesmos overrides de um input/`SelectTrigger`). |
50
+
51
+ ## Exemplo mínimo
52
+ ```tsx
53
+ // single (default)
54
+ const [date, setDate] = useState<Date>();
55
+ <DatePicker value={date} onValueChange={setDate} placeholder="Data de nascimento" />
56
+
57
+ // range
58
+ const [range, setRange] = useState<DateRange>();
59
+ <DatePicker mode="range" value={range} onValueChange={setRange} />
60
+
61
+ // multiple
62
+ const [dates, setDates] = useState<Date[]>();
63
+ <DatePicker mode="multiple" value={dates} onValueChange={setDates} />
64
+ ```
65
+
66
+ ## Cuidados / Gotchas
67
+ - **Composto sobre `Popover` + `Calendar` do DS** (`@/components/shadcn/popover` e
68
+ `@/components/shadcn/calendar`, este último em cima de `react-day-picker`). Copiando via
69
+ registry essas deps já vêm junto (`date-picker` declara `@igreen/calendar` +
70
+ `@igreen/popover` como `registryDependencies`); copiando manual, garanta que os dois
71
+ existem no consumidor antes.
72
+ - **`mode` muda o TIPO, não só o comportamento.** `single` é opcional (default), mas
73
+ `range`/`multiple` exigem a prop explícita. Trocar o modo de um DatePicker controlado
74
+ exige trocar o estado (`Date` → `DateRange`/`Date[]`) junto — o TS não deixa passar
75
+ `value` do shape errado se a discriminated union for respeitada.
76
+ - Em `range`, o `Calendar` interno recebe `min={1}` — sem isso o react-day-picker fecharia
77
+ o range já no primeiro clique (`from`/`to` iguais no mesmo dia).
78
+ - Em `multiple`, não há botão "Aplicar"/"Fechar" embutido — o popover some só no
79
+ clique-fora.
80
+ - **Não há pass-through de restrição de datas** (desabilitar dias específicos / limitar o
81
+ intervalo navegável) pro `Calendar` interno — a prop `disabled` do DatePicker desabilita
82
+ o trigger inteiro, não datas específicas do calendário.
83
+ - O label do trigger é formatado em pt-BR fixo (`toLocaleDateString("pt-BR", ...)`); não
84
+ existe prop de formato customizável.
@@ -0,0 +1,59 @@
1
+ # DateSeparatorChip
2
+
3
+ **Categoria:** composto (Chip + Icon + Separator). Separador **centralizado na thread** do chat/atendimento. Marcador estático — não interativo.
4
+
5
+ ## Quando usar
6
+
7
+ - **Separador de data** numa lista de mensagens (`MessagesList`): "Hoje", "Ontem", "22 jun".
8
+ - **Limite de conversa** (`boundary`): "Conversa encerrada" / "Conversa iniciada" — chip ladeado por duas réguas finas.
9
+
10
+ Não use para status de mensagem individual (use `Chip`) nem para divisões de seção genéricas fora da thread (use `Separator`).
11
+
12
+ ## Anatomia
13
+
14
+ ```
15
+ variant="date" variant="boundary"
16
+ ┌──────────┐ ────── ┌────────────────────┐ ──────
17
+ │ Hoje │ │ ✓ Conversa encerrada│
18
+ └──────────┘ └────────────────────┘
19
+ (chip pílula neutra, (chip entre 2 réguas bg-border;
20
+ centralizado mx-auto) ícone opcional à esquerda)
21
+ ```
22
+
23
+ ## Props essenciais
24
+
25
+ | Prop | Tipo | Default | Descrição |
26
+ |------|------|---------|-----------|
27
+ | `label` | `string` | — | **Obrigatório.** Texto centralizado da pílula. |
28
+ | `variant` | `"date" \| "boundary"` | `"date"` | `date` = só o chip; `boundary` = chip entre 2 réguas. |
29
+ | `icon` | `IconName` | — | Ícone opcional à esquerda do label (ex: `line-check-circle` em boundary). |
30
+ | `className` | `string` | — | className do container (root). |
31
+
32
+ ## Exemplo mínimo
33
+
34
+ ```tsx
35
+ import { DateSeparatorChip } from "@snksergio/design-system";
36
+
37
+ // Separador de data na MessagesList
38
+ <DateSeparatorChip label="Hoje" />
39
+
40
+ // Limite de conversa
41
+ <DateSeparatorChip
42
+ variant="boundary"
43
+ icon="line-check-circle"
44
+ label="Conversa encerrada"
45
+ />
46
+ ```
47
+
48
+ ## Variants
49
+
50
+ | Variant | Valores | Efeito |
51
+ |---------|---------|--------|
52
+ | `variant` | `date` · `boundary` | `date` = chip centralizado isolado; `boundary` = chip ladeado por 2 réguas finas (`bg-border` do Separator) ocupando o espaço lateral |
53
+
54
+ ## Gotchas
55
+
56
+ - **Composição pura:** reusa `Chip` (color `neutral`, variant `soft`, size `sm` → pílula `rounded-radius-full`, `text-caption-sm`), `Icon` (`size="xs"` = 12px) e `Separator` do shadcn. Não reescreve nenhum deles.
57
+ - **Réguas:** as réguas de `boundary` são `<Separator>` do shadcn com só `flex-1` (slot `rule`) pra esticar — a cor `bg-border` já vem do próprio Separator, sem override; centralização do chip via `mx-auto` + `gap-gp-md` lateral.
58
+ - **Estático:** sem foco/disabled — é um marcador, não um controle. Renderiza `role="separator"` + `aria-label={label}`.
59
+ - **Acessibilidade do ícone:** o `Icon` aqui é decorativo (sem `title`) — o significado já vem no `label` lido pelo `aria-label` do separador.
@@ -0,0 +1,72 @@
1
+ # EmptyState
2
+
3
+ **Categoria:** composto (Icon/lucide + Button). Estado vazio genérico, reusável no app inteiro — sem dados, busca sem resultado, conversa não selecionada, lista/inbox vazia, erro de carregamento, etc.
4
+
5
+ ## Quando usar
6
+
7
+ - Uma área não tem conteúdo a exibir e você quer comunicar o porquê + (opcional) uma ação para sair do vazio.
8
+ - Tela de chat sem conversa selecionada, tabela/lista sem itens, resultado de busca/filtro vazio.
9
+
10
+ Não use para erros bloqueantes de página inteira com retry de sistema (use um padrão de erro dedicado) nem para loading (use skeleton/spinner).
11
+
12
+ ## Anatomia
13
+
14
+ ```
15
+ [ ícone ] ← size-icon-xl (sm) / 2xl (md,lg), cor fg-subtle
16
+ Título ← text-title-sm (sm,md) / md (lg), fg-strong
17
+ Descrição opcional ← body-sm, fg-muted, max-w 360px
18
+ [ Button ] ← ação opcional (mt de respiro)
19
+ ```
20
+
21
+ Tudo centralizado vertical e horizontalmente, texto centralizado.
22
+
23
+ ## Props essenciais
24
+
25
+ | Prop | Tipo | Default | Descrição |
26
+ |------|------|---------|-----------|
27
+ | `title` | `string` | — | **Obrigatório.** Título principal. |
28
+ | `icon` | `LucideIcon \| ReactNode` | — | Componente lucide (ex: `Inbox`) **ou** node (Icon do DS, ilustração). Componente é dimensionado pelo `size`; node herda o tamanho via `[&_svg]:size-full`. |
29
+ | `description` | `string` | — | Texto auxiliar sob o título. |
30
+ | `action` | `{ label, onClick, color?, variant? } \| ReactNode` | — | Objeto vira um `<Button>` do DS; ou passe um node custom (2 botões, link...). |
31
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Tamanho do ícone + tipografia do título + size do Button da ação. |
32
+ | `className` | `string` | — | className do container. |
33
+
34
+ ## Variants
35
+
36
+ | `size` | Ícone | Título | Button da ação |
37
+ |--------|-------|--------|----------------|
38
+ | `sm` | `size-icon-xl` (32px) | `text-title-sm` | `sm` |
39
+ | `md` | `size-icon-2xl` (40px) | `text-title-sm` | `md` |
40
+ | `lg` | `size-icon-2xl` (40px) | `text-title-md` | `lg` |
41
+
42
+ ## Exemplo mínimo
43
+
44
+ ```tsx
45
+ import { Inbox } from "lucide-react";
46
+ import { EmptyState } from "@/components/ui/EmptyState";
47
+
48
+ // Lista vazia com ação
49
+ <EmptyState
50
+ icon={Inbox}
51
+ title="Nenhuma conversa selecionada"
52
+ description="Selecione uma conversa ao lado para visualizar as mensagens."
53
+ action={{ label: "Iniciar atendimento", onClick: handleStart }}
54
+ />
55
+
56
+ // Só ícone + título (busca sem resultado)
57
+ <EmptyState size="sm" icon={Search} title="Nenhum resultado encontrado" />
58
+
59
+ // Ícone custom via Icon do DS + ação custom (node)
60
+ <EmptyState
61
+ icon={<Icon name="line-mail" />}
62
+ title="Caixa de entrada vazia"
63
+ action={<MyCustomButtons />}
64
+ />
65
+ ```
66
+
67
+ ## Gotchas / cuidados
68
+
69
+ - **`icon` aceita componente OU node.** Passe a referência do componente lucide (`icon={Inbox}`, sem `<>`), que ele é instanciado e dimensionado pelo `size`. Para um `<Icon>` do DS ou ilustração, passe o elemento (`icon={<Icon name="..." />}`) — o wrapper aplica cor `fg-subtle` e o SVG ocupa o tamanho via `[&_svg]:size-full`.
70
+ - **`action` é discriminada em runtime:** objeto plano com `label`+`onClick` vira Button; qualquer element React é renderizado como está. Para customizar a cor/variante do Button, use `{ label, onClick, color, variant }`.
71
+ - Componente declarativo de **display** — não tem foco próprio nem `disabled`. O único elemento interativo é o Button da ação (já traz o Padrão 1 de foco do DS).
72
+ - O título usa `<h3>`; garanta hierarquia de headings coerente na página que o consome.
@@ -0,0 +1,95 @@
1
+ # FileUploadField — USAGE
2
+
3
+ Captura de **um** arquivo com preview. Componente **dumb**: só captura o `File` e
4
+ mostra preview — o **consumidor faz o upload**. Categoria: form / data-input.
5
+
6
+ Composto de `FormField` (label/estado/erro) + `Button` + `Chip` + `Icon` + `tv()`.
7
+
8
+ ## Quando usar
9
+
10
+ - Anexar avatar, logo, comprovante, documento único em um formulário.
11
+ - Quando o upload é responsabilidade do consumer (envia o `File` pra API depois).
12
+ - Para **múltiplos** arquivos ou drag & drop, NÃO use este (single-file; D&D é fase 2).
13
+
14
+ ## Import
15
+
16
+ ```tsx
17
+ import { FileUploadField } from "@/components/ui/FileUploadField";
18
+ ```
19
+
20
+ ## Props essenciais
21
+
22
+ | Prop | Tipo | Default | Descrição |
23
+ |---|---|---|---|
24
+ | `value` | `File \| string \| null` | — (req) | `File` recém-selecionado, `string` (URL hospedada) ou `null` (vazio) |
25
+ | `onChange` | `(file: File \| null) => void` | — (req) | Selecionar → `File`; remover → `null` |
26
+ | `accept` | `string` | — | Filtro do seletor + validação (`"image/*"`, `".pdf,.png"`) |
27
+ | `maxSizeMB` | `number` | — | Tamanho máximo; acima → rejeitado via `onError("size")` |
28
+ | `preview` | `"image" \| "file" \| "auto"` | `"auto"` | Modo de preview; `auto` infere por accept/MIME |
29
+ | `fileName` | `string` | — | Nome exibido quando `value` é uma URL |
30
+ | `label` | `string` | — | Label (renderizado pelo FormField) |
31
+ | `required` | `boolean` | — | Asterisco no label |
32
+ | `state` | `"default" \| "error" \| "warning" \| "success"` | `"default"` | Estado semântico do FormField |
33
+ | `errorMessage` | `string` | — | Mensagem quando `state="error"` |
34
+ | `helperText` | `string` | — | Texto auxiliar abaixo do campo |
35
+ | `onError` | `(reason: "type" \| "size") => void` | — | Disparado quando arquivo é rejeitado |
36
+ | `disabled` | `boolean` | — | Desabilita dropzone + remover |
37
+ | `id` | `string` | — | id do input (linka label↔input) |
38
+ | `className` | `string` | — | className do container (FormField) |
39
+
40
+ ## Estados visuais
41
+
42
+ | value | Render |
43
+ |---|---|
44
+ | `null` | Dropzone `<button>` full-width (`min-h-form-xl`, dashed) com ícone + "Clique para anexar" + hint accept/maxSize |
45
+ | `File`/URL imagem | Row: thumbnail (`size-icon-2xl`, object-cover) + botão remover |
46
+ | `File`/URL arquivo | Row: Chip soft (ícone file + nome truncate) + botão remover |
47
+
48
+ ## Exemplo mínimo
49
+
50
+ ```tsx
51
+ const [file, setFile] = useState<File | null>(null);
52
+
53
+ <FileUploadField
54
+ label="Comprovante"
55
+ required
56
+ value={file}
57
+ onChange={setFile}
58
+ accept="image/*,.pdf"
59
+ maxSizeMB={5}
60
+ onError={(reason) =>
61
+ toast.error(reason === "size" ? "Arquivo muito grande" : "Tipo inválido")
62
+ }
63
+ />
64
+ ```
65
+
66
+ ### Editar registro existente (value como URL)
67
+
68
+ ```tsx
69
+ <FileUploadField
70
+ label="Logo"
71
+ value={logoUrl} // string (URL já hospedada)
72
+ fileName="logo.png"
73
+ onChange={(f) => setNewLogo(f)} // null = remover, File = trocar
74
+ accept="image/*"
75
+ preview="image"
76
+ />
77
+ ```
78
+
79
+ ## Gotchas / cuidados
80
+
81
+ - **Dumb component**: não envia nada à rede. O `onChange(File)` te dá o `File`;
82
+ faça `FormData`/upload no consumer e persista a URL retornada.
83
+ - Arquivo rejeitado (tipo/tamanho) **não** vira `value` — só dispara `onError`.
84
+ Para feedback visual no campo, controle `state="error"` + `errorMessage` no consumer.
85
+ - Preview de `File` usa `URL.createObjectURL` (revogado automaticamente). Para URL
86
+ string, o `src` é o próprio value.
87
+ - Single-file: o input não tem `multiple`. Drag & drop é fase 2 (API preservada).
88
+ - A11y: dropzone é `<button>` (Enter/Space abrem o seletor); input file hidden com
89
+ `id` do FormField; thumbnail tem `alt`; remover é icon-only com `aria-label`.
90
+
91
+ ## Fonte de verdade
92
+
93
+ - Estilos: `file-upload-field.styles.ts` (tv())
94
+ - Lógica: `file-upload-field.tsx`
95
+ - Tipos: `file-upload-field.types.ts`
@@ -0,0 +1,118 @@
1
+ # FloatingPanel — USAGE
2
+
3
+ <!-- ds:regras
4
+ - painel de DETALHE → siga o bloco `dsgreen-paneldetail-1`, que é o **padrão**. As variações são opt-in: use `-2` (tarefa com abas) ou `-3` (com tabela, `size="xl"`) **só se o usuário citar o ID**
5
+ - em todos: identidade ou contexto no `titleSlot`, ações de ícone `soft` + `aria-label` no `headerActions`, ação primária no `footer` — nunca botão de ação solto no corpo
6
+ - `bodyPadded={false}` quando usar `FloatingPanelSection` (a section gerencia padding e divisória full-width)
7
+ - é REDIMENSIONÁVEL em runtime — nenhuma largura escrita na mão acompanha o arrasto
8
+ - aba dentro dele → `<Tabs fullWidth>` na variante default; `line` aqui vira trilho curto
9
+ -->
10
+
11
+ Drawer card flutuante non-modal — resizável, maximizável, coexiste com página atrás (sem backdrop).
12
+
13
+ ## Quando usar
14
+ - Detail panel que precisa ficar visível enquanto user interage com lista atrás
15
+ - Side-drawer leve sem bloquear navegação
16
+ - Painel de propriedades estilo Figma/Notion
17
+
18
+ ## Import
19
+ ```tsx
20
+ import {
21
+ FloatingPanel,
22
+ FloatingPanelSection, // seção colapsável (detail panel)
23
+ FloatingPanelField, // linha label : valor
24
+ } from "@/components/ui/FloatingPanel";
25
+ ```
26
+
27
+ ## Variants
28
+ | Variant | Valores | Default | Quando |
29
+ |---|---|---|---|
30
+ | `side` | left / right | right | Lado de ancoragem |
31
+ | `size` | sm / md / lg / xl / number (px) | md | sm=320, md=400, lg=560, xl=720px. `number` = largura custom em px; com `resizable`, vira a largura inicial |
32
+
33
+ ## Props essenciais
34
+ | Prop | Tipo | Função |
35
+ |---|---|---|
36
+ | `open` | boolean | Visibilidade |
37
+ | `onOpenChange` | (open: boolean) => void | Callback de fechamento |
38
+ | `title` | string | Header title |
39
+ | `description` | string | Header subtitle |
40
+ | `titleIcon` | LucideIcon | Ícone à esquerda do título |
41
+ | `hideClose` | boolean | Esconde o botão X de fechar (default `false`) |
42
+ | `resizable` | boolean | Habilita drag-resize (desabilitado em mobile) |
43
+ | `resizableMinWidth` / `resizableMaxWidth` | number | Bounds do resize em px (defaults `320` / `800`). São **px cegos à viewport** — quem impede o estouro é o teto `md:max-w-[calc(100vw-48px)]` do próprio painel, que mantém o gutter de 24px dos dois lados. Antes dele, props default numa janela de 800px punham a borda esquerda em **-24px** (800 de largura + 24 de gutter = 824 necessários) |
44
+ | `resizableStorageKey` | string | Chave do localStorage pra persistir width entre sessões |
45
+ | `maximizable` | boolean | Botão de expandir pra fullscreen |
46
+ | `headerActions` | ReactNode | Slots no header |
47
+ | `footer` | ReactNode | Slot do footer sticky |
48
+ | `titleSlot` | ReactNode | Override total do header (avatar + nome custom) |
49
+ | `bodyPadded` | boolean | Padding interno padrão do body (gutter 18px). **Default `true`** — conteúdo livre já respira. Use `false` com `<FloatingPanelSection>` (sections gerenciam o próprio padding edge-to-edge) |
50
+
51
+ ## Exemplo mínimo (conteúdo livre — padding automático)
52
+ ```tsx
53
+ <FloatingPanel
54
+ open={panelOpen}
55
+ onOpenChange={setPanelOpen}
56
+ side="right"
57
+ size="md"
58
+ title="Detalhes do cliente"
59
+ description="Última edição há 2h"
60
+ resizable
61
+ maximizable
62
+ footer={<><Button variant="ghost">Cancelar</Button><Button>Salvar</Button></>}
63
+ >
64
+ {/* bodyPadded=true (default) → conteúdo já tem gutter, não precisa de p-* manual */}
65
+ <ClientDetails />
66
+ </FloatingPanel>
67
+ ```
68
+
69
+ ## Detail panel padrão → o bloco `dsgreen-paneldetail-1`
70
+
71
+ **Painel de detalhe de registro tem estrutura definida, e ela é referenciável por ID.** A
72
+ composição inteira (com fixture, medições e o porquê de cada decisão) vive em
73
+ `src/blocks/paneldetail/` e é renderizada em `#/blocks-paneldetail`. Cite o ID em vez de
74
+ recompor: `use a referência dsgreen-paneldetail-1 no painel de detalhe do pedido`.
75
+
76
+ As quatro zonas, que são o que a IA erra quando compõe do zero — ela joga tudo no corpo:
77
+
78
+ | zona | o que vai | por que |
79
+ |---|---|---|
80
+ | `titleSlot` | avatar + nome + código · Chip de status | responde "de quem é este painel"; fica fixo no scroll |
81
+ | `headerActions` | 1–2 ações de ícone, **sempre `variant="soft"`** | `ghost` no meio da fileira fica sem container e lê como desabilitada ao lado do maximize/close, que são `soft` |
82
+ | corpo, 1ª seção | métricas em **cards compactos** (não `Kpi`) | responde "como este registro está?" — vem antes dos campos, que respondem "quais são os dados" |
83
+ | corpo, resto | campos em `FloatingPanelSection` por assunto | o colapso é o que permite 20 campos sem obrigar a rolar 20 |
84
+ | `footer` | Fechar + ação primária | a ação que fecha a tarefa, sempre alcançável |
85
+
86
+ ⛔ **Não use aba aqui.** Se o corpo já é pilha de seções colapsáveis, o colapso **é** o
87
+ mecanismo de esconder — ter os dois faz o usuário procurar o dado em dois lugares. Recorte
88
+ volumoso de verdade (extrato de 200 linhas) não é aba nem seção: é outra tela.
89
+
90
+ ### Anatomia mínima
91
+
92
+ Use `bodyPadded={false}` (as sections têm padding + divisória própria):
93
+
94
+ ```tsx
95
+ <FloatingPanel open={open} onOpenChange={setOpen} bodyPadded={false} titleSlot={<HeaderCustom />}>
96
+ <FloatingPanelSection title="Contato"> {/* colapsável (default) */}
97
+ <FloatingPanelField label="Email" value={<a href={...}>{email}</a>} />
98
+ <FloatingPanelField label="Telefone" value={phone} />
99
+ </FloatingPanelSection>
100
+ <FloatingPanelSection title="Financeiro" defaultOpen={false}>
101
+ <FloatingPanelField label="Saldo" value={formatBRL(saldo)} />
102
+ </FloatingPanelSection>
103
+ </FloatingPanel>
104
+ ```
105
+
106
+ - `FloatingPanelSection` — props: `title`, `collapsible` (default `true`), `defaultOpen` (default `true`).
107
+ - `FloatingPanelField` — props: `label`, `value` (fallback "—" quando vazio).
108
+
109
+ ## Cuidados / Gotchas
110
+ - **Aba dentro do FloatingPanel** → `<Tabs fullWidth>` com a variante **default** (`segmented`). Aqui é o caso mais forte da prop: o painel é **redimensionável em runtime** (`sm` 320 · `md` 400 · `lg` 560 · `xl` 720, e o usuário arrasta), então nenhuma largura escrita na mão acompanha — `fullWidth` distribui os triggers e a aba segue o arrasto.
111
+
112
+ - Renderizado via **portal em document.body** — escapa de overflow/transform ancestrais
113
+ - Em mobile (<md) vira **sheet bottom-up colado nas bordas** do device: flush nas laterais + bottom, só cantos superiores arredondados, sem outline/shadow, `max-height: 92vh`
114
+ - **Backdrop só em mobile** — scrim suave (toque fora fecha). No **desktop segue non-modal** (sem backdrop, página atrás clicável). Pra modal full use `<Modal>` ou `<AlertModal>`
115
+ - **Body com scroll automático** (`overflow-y-auto` + `min-h-0`) — header/footer ficam fixos, conteúdo longo rola
116
+ - **Footer fluido**: botões crescem lado a lado e **empilham quando não cabem** (`flex-wrap` + `flex-1` + `min-w-140px`). Não precisa passar `fullWidth` nos Buttons
117
+ - `maximizable=true` adiciona botão de expandir; estado controlado internamente — `defaultMaximized` (default `false`) inicia maximizado
118
+ - **ESC fecha por padrão** (`closeOnEscape` default `true`) — passe `false` pra desativar
@@ -0,0 +1,62 @@
1
+ # FooterTable — USAGE
2
+
3
+ Footer de tabela com paginação + page-size select + range display + selection count.
4
+
5
+ ## Quando usar
6
+ - Rodapé de qualquer `<Table>` ou `<DataTable>` (já é embutido no DataTable)
7
+ - Listas paginadas com seleção múltipla
8
+
9
+ ## Import
10
+ ```tsx
11
+ import { FooterTable } from "@/components/ui/FooterTable";
12
+ ```
13
+
14
+ ## Props essenciais
15
+ | Prop | Tipo | Default | Função |
16
+ |---|---|---|---|
17
+ | `totalCount` | number | — | Total de registros |
18
+ | `page` | number | — | Página atual (1-indexed) |
19
+ | `pageSize` | number | — | Tamanho da página atual |
20
+ | `onPageChange` | (page: number) => void | — | Callback de paginação |
21
+ | `onPageSizeChange` | (size: number) => void | — | Callback de troca de page-size |
22
+ | `pageSizeOptions` | number[] | [10, 25, 50, 100] | Opções do select |
23
+ | `selectionCount` | number | 0 | Quantos selecionados (acrescenta "· N selecionado(s)" ao lado do range) |
24
+ | `pageSizeLabel` | string | "Linhas" | Label do select de page-size |
25
+ | `rowLabel` | string | "rows" | Sufixo do range (ex "registros") |
26
+ | `locale` | string | "pt-BR" | Locale pra formatar números do range |
27
+ | `hidePageSize` | boolean | false | Esconde select de page-size |
28
+ | `hideRange` | boolean | false | Esconde "1–10 de 87 rows" |
29
+ | `hideFirstLast` | boolean | false | Esconde botões « » |
30
+
31
+ ## Exemplo mínimo
32
+ ```tsx
33
+ <FooterTable
34
+ totalCount={87}
35
+ page={currentPage}
36
+ pageSize={10}
37
+ onPageChange={setPage}
38
+ onPageSizeChange={setPageSize}
39
+ selectionCount={selectedIds.length}
40
+ />
41
+ ```
42
+
43
+ ## FooterTableSkeleton
44
+ Estado loading do footer — usado pelo DataTable enquanto a primeira chamada de `fetchData` não retornou `totalCount`. Mantém a mesma silhueta do footer real pra evitar layout shift.
45
+
46
+ ```tsx
47
+ import { FooterTableSkeleton } from "@/components/ui/FooterTable";
48
+
49
+ <FooterTableSkeleton pageButtonCount={5} />
50
+ ```
51
+
52
+ | Prop | Tipo | Default | Função |
53
+ |---|---|---|---|
54
+ | `pageButtonCount` | number | 5 | Quantos botões de página simular |
55
+ | `className` | string | — | className extra no `<footer>` |
56
+
57
+ ## Cuidados / Gotchas
58
+ - Calcula range "1–10 de 87 rows" automaticamente (sufixo customizável via `rowLabel`)
59
+ - Empilha vertical em mobile (`flex-col <sm`) — layout fixo, sem custom
60
+ - `selectionCount > 0` acrescenta "· N selecionado(s)" ao lado do range — NÃO substitui o range; pra esconder o range use `hideRange`
61
+ - Default `rowLabel = "rows"` produz texto misto ("1–10 de 87 rows") — passe `rowLabel="registros"` pra texto 100% pt-BR
62
+ - Pra usar fora de tabela, pode ser standalone — props são puramente paginação
@@ -0,0 +1,110 @@
1
+ # FormField — USAGE
2
+
3
+ Container de form com label + field + mensagem de validação (error/warning/success). Inclui wrappers "one-shot" (`FormFieldInput`, `FormFieldSelect`, etc) — a forma recomendada de consumo em forms (L-023).
4
+
5
+ ## Quando usar
6
+
7
+ - Qualquer campo de formulário que tenha label + input + validação
8
+ - Garantir spacing e tipografia consistente entre campos
9
+ - NUNCA escrever `<label>` raw com classes manuais em forms (L-023)
10
+
11
+ ## Import
12
+
13
+ ```tsx
14
+ import {
15
+ FormField, // base (children render-prop) — para widgets custom
16
+ FormFieldInput, // wrappers one-shot (recomendados)
17
+ FormFieldTextarea,
18
+ FormFieldSelect,
19
+ FormFieldCheckbox,
20
+ FormFieldSwitch,
21
+ } from "@/components/ui/FormField";
22
+ import type { FieldState, FormFieldSelectOption } from "@/components/ui/FormField";
23
+ ```
24
+
25
+ ## Props essenciais
26
+
27
+ ### `FormFieldBaseProps` — comuns a todos
28
+
29
+ | Prop | Tipo | Default | Função |
30
+ | ---------------- | ------------------------------------------------- | ------------- | ------------------------------------------------------------ |
31
+ | `label` | `string` | — | Label acima do field (Checkbox/Switch: `ReactNode`, inline) |
32
+ | `required` | boolean | `false` | Asterisco vermelho ao lado do label (só visual) |
33
+ | `helperText` | `ReactNode` | — | Texto auxiliar abaixo do field |
34
+ | `state` | `"default" \| "error" \| "warning" \| "success"` | `"default"` | Estado semântico — afeta borda do field e mensagem exibida |
35
+ | `errorMessage` | `ReactNode` | — | Exibida quando `state="error"` (substitui o helperText) |
36
+ | `warningMessage` | `ReactNode` | — | Exibida quando `state="warning"` |
37
+ | `successMessage` | `ReactNode` | — | Exibida quando `state="success"` |
38
+ | `id` | `string` | auto (`useId`) | Linka label↔input via `htmlFor` |
39
+ | `className` | `string` | — | className do container externo |
40
+
41
+ ### Específicas do `FormField` (base)
42
+
43
+ | Prop | Tipo | Função |
44
+ | ----------- | ----------------------------------------------------- | ---------------------------------------------------------------------------- |
45
+ | `children` | `(ctx: { id: string; state: FieldState }) => ReactNode` | **Render-prop obrigatório** — recebe o id gerado + state pra repassar ao field |
46
+ | `hideLabel` | boolean | Esconde o label (field self-contained, ex: Checkbox com label inline) |
47
+ | `disabled` | boolean | Esmaece SÓ o label (opacity-50) — o field filho precisa do próprio `disabled` |
48
+
49
+ ### Wrappers one-shot (recomendados — L-023)
50
+
51
+ | Componente | Field interno | Extras |
52
+ | ------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
53
+ | `FormFieldInput` | `Input` / `InputGroup` (shadcn) | `startAddon` / `endAddon` (`ReactNode`; string vira `InputGroupText`, ex `"R$"`) + props de `Input` |
54
+ | `FormFieldTextarea` | `Textarea` (shadcn) | props de `Textarea` |
55
+ | `FormFieldSelect` | `Select` (radix/shadcn) | `options: FormFieldSelectOption[]` (`{ value, label, disabled? }`), `placeholder`, `value`, `defaultValue`, `onValueChange`, `disabled`, `triggerClassName`, `contentClassName` |
56
+ | `FormFieldCheckbox` | `Checkbox` (shadcn) | label inline à direita do checkbox; props do Radix `Checkbox.Root` |
57
+ | `FormFieldSwitch` | `Switch` (shadcn) | label inline; `switchPosition?: "start" \| "end"` (default `"end"`); props do Radix `Switch.Root` |
58
+
59
+ ## Variants (slots internos de estilo)
60
+
61
+ | Slot | Variant | Valores | Default |
62
+ | --------------------------- | ---------- | ------------------------------------ | ------- |
63
+ | Label (`formFieldLabel`) | `disabled` | true/false | false |
64
+ | Message (`formFieldMessage`) | `state` | default / error / warning / success | default |
65
+
66
+ O container (`formFieldRoot`) não tem variants — é base-only (`flex flex-col gap-[7px] w-full`).
67
+
68
+ ## Exemplo mínimo
69
+
70
+ ```tsx
71
+ // Wrapper one-shot — forma recomendada
72
+ <FormFieldInput
73
+ label="Email"
74
+ required
75
+ type="email"
76
+ state="error"
77
+ errorMessage="Email inválido"
78
+ value={email}
79
+ onChange={(e) => setEmail(e.target.value)}
80
+ />
81
+
82
+ // Com addons
83
+ <FormFieldInput label="Valor" startAddon="R$" placeholder="0,00" />
84
+
85
+ // Select
86
+ <FormFieldSelect
87
+ label="País"
88
+ placeholder="Selecione..."
89
+ options={[
90
+ { value: "br", label: "Brasil" },
91
+ { value: "us", label: "Estados Unidos" },
92
+ ]}
93
+ />
94
+
95
+ // Base com widget custom — children é render-prop
96
+ <FormField label="Email" required state="error" errorMessage="Email inválido">
97
+ {({ id, state }) => <Input id={id} state={state} type="email" />}
98
+ </FormField>
99
+ ```
100
+
101
+ ## Cuidados / Gotchas
102
+
103
+ - `children` do `FormField` base é **função** (render-prop), não JSX direto — passar ReactNode é erro de tipo e quebra em runtime
104
+ - NÃO existem componentes `FormFieldLabel`/`FormFieldMessage` — label e mensagem são props (`label`, `errorMessage`, `helperText`...); esses nomes existem só como slots internos em `form-field.styles.ts`
105
+ - Prioridade da mensagem: error → warning → success → helperText. A mensagem do state só aparece se a prop correspondente (`errorMessage`, etc) for passada
106
+ - O `state` é propagado ao field via argumento do render-prop (`ctx.state`) — não há context; em widget custom, repasse `id` e `state` manualmente
107
+ - `disabled` no `FormField` base esmaece apenas o label — passe `disabled` também ao field. Os wrappers (`FormFieldInput`, etc) propagam automaticamente
108
+ - Mensagem com `state="error"` recebe `role="alert"` (acessibilidade)
109
+ - Spacing label/field/message é do container — não adicionar margins customizados; entre fields empilhados use `gap-form-gap` (L-024)
110
+ - `required` é só visual — validação real é responsabilidade do consumer