@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,128 @@
1
+ # MenuSidebar — USAGE
2
+
3
+ Sidebar composto: rail 64px (ícones de contexts) + panel 264px (items do context ativo, colapsável).
4
+
5
+ ## Quando usar
6
+ - Navegação primária de app multi-context (Inbox, Clientes, Configurações, etc)
7
+ - Dentro de `<AppShell>` (template canônico) ou standalone em apps custom
8
+
9
+ ## Import
10
+ ```tsx
11
+ import { MenuSidebar } from "@/components/ui/MenuSidebar";
12
+ ```
13
+
14
+ ## Props essenciais
15
+ | Prop | Tipo | Função |
16
+ |---|---|---|
17
+ | `contexts` | SidebarContext[] | Lista de workspaces (rail) — `{ id, label, icon, items, sections? }` |
18
+ | `activeContextId` | string | Context ativo (controlled) |
19
+ | `defaultActiveContextId` | string | Context inicial (uncontrolled) |
20
+ | `onContextChange` | (id: string) => void | Callback de troca de context |
21
+ | `activeItemHref` | string | Item ativo no panel (controlled, match por href) |
22
+ | `defaultActiveItemHref` | string | Item ativo inicial (uncontrolled) |
23
+ | `onItemClick` | (item, event?) => void | Callback ao clicar em item do panel. **O 2º arg é o `MouseEvent`** — use pra `preventDefault()` se você roteia na mão |
24
+ | `renderLink` | (props) => ReactNode | ⭐ **Integração com router** — substitui o `<a>` interno pelo `<Link>` do seu router. Ver a seção abaixo |
25
+ | `brandHref` | string | Destino do brand no rail. Default `"/"`; `""` torna não-navegável (vira `<button>`) |
26
+ | `onBrandClick` | (e) => void | Clique no brand |
27
+ | `panelCollapsed` | boolean | Panel colapsado — só rail visível (controlled) |
28
+ | `defaultPanelCollapsed` | boolean | Panel colapsado inicial (uncontrolled) |
29
+ | `onPanelCollapseChange` | (collapsed: boolean) => void | Callback do collapse |
30
+ | `expandOnHover` | boolean (default `true`) | Com panel colapsado, hover abre o panel como overlay absoluto (não empurra conteúdo) |
31
+ | `mobileOpen` | boolean | Drawer mobile aberto (controlled) |
32
+ | `defaultMobileOpen` | boolean (default `false`) | Drawer mobile inicial (uncontrolled) |
33
+ | `onMobileOpenChange` | (open: boolean) => void | Callback do drawer mobile |
34
+ | `mobileBreakpoint` | string (default `"(max-width: 767px)"`) | Media query que ativa o modo mobile (matchMedia) |
35
+ | `brand` | ReactNode | Logo/avatar no topo do rail |
36
+ | `user` | ReactNode | Avatar+menu no bottom do rail |
37
+
38
+ ## Exemplo mínimo
39
+ ```tsx
40
+ <MenuSidebar
41
+ contexts={MOCK_CONTEXTS}
42
+ defaultActiveContextId="inbox"
43
+ defaultActiveItemHref="#chat"
44
+ brand={<Logo />}
45
+ user={<UserMenu user={currentUser} />}
46
+ />
47
+ ```
48
+
49
+ ## 🧭 Integração com router — LEIA se seu app tem rotas
50
+
51
+ O sidebar renderiza `<a href={item.href}>`. Sem integração, clicar num item com `href` de
52
+ **path** (`/app/clientes`) faz o browser **recarregar a página inteira** — foi um bug real,
53
+ reportado por consumidor em 2026-08-08 e corrigido na v0.38.0.
54
+
55
+ ### Recomendado: `renderLink`
56
+
57
+ ```tsx
58
+ import { Link } from "react-router-dom";
59
+
60
+ <MenuSidebar
61
+ contexts={contexts}
62
+ renderLink={(p) => <Link {...p} to={p.href} />}
63
+ />
64
+ ```
65
+
66
+ | Router | `renderLink` |
67
+ |---|---|
68
+ | **react-router** | `(p) => <Link {...p} to={p.href} />` |
69
+ | **Next.js** | `(p) => <Link {...p} />` (já usa `href`) |
70
+ | **TanStack Router** | `(p) => <Link {...p} to={p.href} />` |
71
+
72
+ Com `renderLink`, o sidebar **não** mexe em `preventDefault` — quem decide é o `<Link>`.
73
+ Você ganha navegação client-side **e** mantém ctrl/cmd+clique pra abrir em nova aba.
74
+
75
+ > ⚠️ **É render-prop, não `linkComponent`.** Um prop que recebe *tipo de componente* e é
76
+ > escrito inline cria um tipo novo a cada render, e o React desmonta/remonta a subárvore —
77
+ > perde foco, reinicia animação, e o sintoma parece aleatório. Render-prop inline é seguro.
78
+
79
+ ### Alternativa: `onItemClick` + `navigate()`
80
+
81
+ Se preferir rotear na mão, funciona sem `renderLink` — o sidebar cancela a navegação
82
+ nativa automaticamente quando você passa `onItemClick`:
83
+
84
+ ```tsx
85
+ const navigate = useNavigate();
86
+ <MenuSidebar contexts={contexts} onItemClick={(item) => item.href && navigate(item.href)} />
87
+ ```
88
+
89
+ O 2º argumento é o evento, se você precisar dele: `onItemClick={(item, e) => { … }}`.
90
+
91
+ ### O que o cancelamento automático NÃO faz (de propósito)
92
+
93
+ Ele **nunca** cancela nestes 5 casos, e cada um quebraria algo real:
94
+
95
+ | Caso | Por que não cancela |
96
+ |---|---|
97
+ | ctrl/cmd/shift/alt ou botão do meio | é como o usuário abre em nova aba |
98
+ | `target: "_blank"` no item | o item pediu outra aba |
99
+ | `href` externo (`https:`, `mailto:`, `tel:`, `//host`) | não é rota do app; cancelar deixa o link morto |
100
+ | **`href` de hash (`#/rota`)** | hash router escuta `hashchange`; cancelar impede o fragmento de mudar e o evento **nunca dispara** |
101
+ | sem `onItemClick` nem `item.onClick` | ninguém trataria — o `<a>` é a navegação pretendida |
102
+
103
+ A regra vive em **`@/utils/nav-link`** e é exportada (`shouldPreventNavigation`,
104
+ `isExternalHref`, `isHashHref`, `isModifiedClick`) pra quem compõe com `<SidebarItem>`
105
+ avulso — reimplementar na unha é como o bug volta.
106
+
107
+ > ⚠️ **Mudou de lugar em 2026-08-18**: era `MenuSidebar/nav-link.ts`, virou
108
+ > `src/utils/nav-link.ts`. O `SingleMenuSidebar` passou a precisar da MESMA regra, e as
109
+ > alternativas eram piores: duplicar cria duas cópias divergindo, e importar do MenuSidebar
110
+ > faria o `single-menu-sidebar` depender do item de registry do MenuSidebar **inteiro** por
111
+ > 60 linhas puras. Agora é util compartilhada, embutida como `registry:file` nos dois itens
112
+ > — mesmo padrão do `@/utils/color-contrast`.
113
+ >
114
+ > **O re-export por `@/components/ui/MenuSidebar` continua valendo**, então quem importava
115
+ > daqui não muda nada. Só quem fazia deep-import do arquivo (`.../MenuSidebar/nav-link`)
116
+ > precisa apontar pro caminho novo.
117
+
118
+ > **Por que passou meses invisível:** o exemplo canônico
119
+ > (`src/examples/app-shell/nav-data.ts`) usa `href` de **hash** em todos os itens, e hash
120
+ > não recarrega documento. Showcase verde, consumidor quebrado.
121
+
122
+ ## Cuidados / Gotchas
123
+ - `w-fit` no root é crítico — sem isso o hover-to-expand dispararia em qualquer lugar do parent
124
+ - Mobile é auto-detectado via `mobileBreakpoint` (matchMedia) — NÃO existe prop `mobile`. Vira drawer fixed overlay (translate-x lateral); backdrop scrim e botão de fechar (X) são renderizados automaticamente pelo próprio MenuSidebar. Consumer só controla `mobileOpen` (ex: hamburger no header)
125
+ - `floating` NÃO é prop pública do MenuSidebar — é prop interna de `<SidebarPanel>` (composição manual). No all-in-one, o overlay flutuante é gerenciado por `expandOnHover` + panel colapsado
126
+ - Items hierárquicos: `items: [{ name, href, subitems: [...] }]` — subitems renderizam indentados; `defaultOpen` define o estado inicial do grupo
127
+ - Context pode ter `sections?: SidebarSection[]` (variants `bookmark` | `chat`) renderizadas abaixo dos items do panel
128
+ - Bookmark item aceita `icon?` opcional: presente → ícone colorido (tingido com `color`, sem fundo; estilo atalho, ex.: ferramentas/integrações); ausente → dot redondo. `color` vale pra ambos. `onAdd?` na section renderiza o botão "+" no header (ex.: abrir catálogo)
@@ -0,0 +1,53 @@
1
+ # MessageAck — USAGE
2
+
3
+ Glifo de status/ack de mensagem (estilo WhatsApp/Baileys) — ícone + cor semântica derivados do valor de `ack`. Componente composto / atômico de chat.
4
+
5
+ ## Quando usar
6
+ - Indicar entrega/leitura de mensagens enviadas (bolha de chat)
7
+ - Rodapé da bolha, ao lado do timestamp
8
+ - Estado de erro de envio (com `error`)
9
+
10
+ ## Import
11
+ ```tsx
12
+ import { MessageAck } from "@/components/ui/MessageAck";
13
+ ```
14
+
15
+ ## Props essenciais
16
+ | Prop | Tipo | Default | Função |
17
+ |---|---|---|---|
18
+ | `ack` | `0 \| 1 \| 2 \| 3 \| 4 \| 5` | — (obrigatória) | Estado de entrega da mensagem |
19
+ | `error` | boolean | `false` | Sobrepõe o ack e mostra glifo de erro |
20
+ | `className` | string | — | Classes extras no ícone |
21
+
22
+ ## Mapa ack → ícone + cor
23
+ | ack | Significado | Ícone | Cor |
24
+ |---|---|---|---|
25
+ | 0 / 1 | Pendente / enviando | Clock | `fg-muted` |
26
+ | 2 | Enviado ao servidor | Check | `fg-muted` |
27
+ | 3 | Entregue | CheckCheck | `fg-muted` |
28
+ | 4 / 5 | Lido / reproduzido | CheckCheck | `fg-success` |
29
+ | — (error) | Falha no envio | AlertCircle | `fg-danger` |
30
+
31
+ ## Exemplo mínimo
32
+ ```tsx
33
+ // Mensagem entregue
34
+ <MessageAck ack={3} />
35
+
36
+ // Mensagem lida (duplo check verde)
37
+ <MessageAck ack={4} />
38
+
39
+ // Falha no envio
40
+ <MessageAck ack={1} error />
41
+
42
+ // No rodapé da bolha, junto ao timestamp
43
+ <span className="inline-flex items-center gap-gp-xs text-fg-muted">
44
+ 10:42
45
+ <MessageAck ack={message.ack} error={message.failed} />
46
+ </span>
47
+ ```
48
+
49
+ ## Cuidados / Gotchas
50
+ - Ícone é `size-icon-xs` fixo — para outro tamanho, passar `className` (override de `size-*`).
51
+ - Decorativo por padrão (`aria-hidden`) — o status é comunicado pelo timestamp/contexto da bolha; não duplica leitura no screen reader.
52
+ - `error` tem precedência sobre `ack` (qualquer ack + error = AlertCircle danger).
53
+ - Cores claro/escuro resolvidas via tokens (`fg-muted`/`fg-success`/`fg-danger`) — não passar cor inline.
@@ -0,0 +1,115 @@
1
+ # MessageBubble
2
+
3
+ **Categoria:** composto (MessageAck + MarkdownText + Icon + Button + Popover + slot Avatar). Bolha de mensagem do **atendimento / chat WhatsApp** — o maior bloco do ChatV2.
4
+
5
+ ## Quando usar
6
+
7
+ - Renderizar uma mensagem dentro de uma timeline de conversa (enviada ou recebida).
8
+ - Suporta texto markdown WA, mídia (imagem/áudio/vídeo/documento/localização/contato), citação (reply), edição, exclusão, status de entrega e ações por hover.
9
+
10
+ Não use para notificações de sistema, separadores de data ou logs de chamada — esses são blocos próprios.
11
+
12
+ ## Anatomia
13
+
14
+ ```
15
+ [avatar] ┌───────────────────────────────┐ ← side="received" (esquerda, bg-surface + border-subtle)
16
+ │ Autor (grupos) ⋮ │ ← ⋮ = actions (Popover, hover top-right)
17
+ │ ▏ citação (reply) ............│ ← quotedMessage (border-l + nome + prévia)
18
+ │ [ mídia / MessageMediaRenderer]│
19
+ │ corpo markdown WA │
20
+ │ editada · HH:mm ✓✓ │ ← meta: editada? + hora + MessageAck (só sent)
21
+ └───────────────────────────────┘
22
+ ┌──────────────────┐ [avatar] ← side="sent" (direita, bg-success-muted)
23
+ │ ... │
24
+ └──────────────────┘
25
+ ```
26
+
27
+ ## Props essenciais
28
+
29
+ | Prop | Tipo | Default | Descrição |
30
+ |------|------|---------|-----------|
31
+ | `side` | `"sent" \| "received"` | — | **Obrigatório.** `sent` = direita (bg verde sutil) · `received` = esquerda (bg surface). |
32
+ | `createdAt` | `string \| Date` | — | **Obrigatório.** Formatada como `HH:mm` (24h). |
33
+ | `body` | `string` | — | Corpo em markdown WA → `MarkdownText`. |
34
+ | `ack` | `0..5` | — | Status de entrega → `MessageAck`. **Só aparece em `side="sent"`.** |
35
+ | `ackError` | `boolean` | `false` | Sobrepõe o `ack` com o glifo de erro de envio (encaminha `error` ao `MessageAck`). Só junto de `ack` em `side="sent"`. |
36
+ | `mediaType` | `text \| image \| audio \| video \| document \| location \| vcard \| contact` | `"text"` | Tipo de mídia (switch interno). |
37
+ | `mediaUrl` | `string` | — | URL da mídia (usada pelo renderer interno). |
38
+ | `media` | `ReactNode` | — | Slot — **override completo** do renderer interno. |
39
+ | `quotedMessage` | `{ authorName?, fromMe?, body?, mediaType?, mediaUrl? } \| null` | — | Citação/reply (barra lateral + prévia truncada). |
40
+ | `isEdited` | `boolean` | `false` | Adiciona label `editada` no rodapé. |
41
+ | `isDeleted` | `boolean` | `false` | Itálico mudo + ícone proibido; **suprime corpo, mídia e ações**. |
42
+ | `tail` | `boolean` | `true` | Rabeta (canto reto) no lado do remetente. |
43
+ | `origin` | `"human" \| "ai"` | `"human"` | `ai` = contorno sutil da marca na bolha (resposta da IA/Sol). **Quem decide é o consumidor**; `human` não muda nada. |
44
+ | `authorName` | `string` | — | Nome no topo (útil em grupos). |
45
+ | `avatar` | `ReactNode` | — | Slot de avatar ao lado da bolha. |
46
+ | `actions` | `ReactNode` | — | Conteúdo do Popover de ações (botão ⋮ aparece no hover). |
47
+ | `onMediaClick` | `() => void` | — | Clique na mídia (ex: abrir lightbox). |
48
+ | `className` | `string` | — | className da linha inteira. |
49
+
50
+ ## Variants
51
+
52
+ | Variant | Valores | Efeito |
53
+ |---------|---------|--------|
54
+ | `side` | `sent` · `received` | bg da bolha + alinhamento + lado da rabeta + lado do ack |
55
+ | `tail` | `true` · `false` | canto reto (`rounded-br-none`/`rounded-bl-none`) vs todos arredondados |
56
+ | `origin` | `human` · `ai` | `ai` troca a borda da bolha por `border-border-brand-subtle` (contorno sutil da marca, nos dois lados); `human` = borda padrão |
57
+
58
+ `disabled` = n/a (bolha não é controle interativo).
59
+
60
+ ## Exemplo mínimo
61
+
62
+ ```tsx
63
+ import { MessageBubble, MessageReplyIcon } from "@snksergio/design-system";
64
+
65
+ <MessageBubble
66
+ side="received"
67
+ authorName="Maria"
68
+ avatar={<Avatar name="Maria" size="sm" />}
69
+ body="Olá! *Tudo certo* com o pedido?"
70
+ createdAt="2026-06-22T14:32:00Z"
71
+ />
72
+
73
+ <MessageBubble
74
+ side="sent"
75
+ body="Sim, já está a caminho 🚚"
76
+ createdAt={new Date()}
77
+ ack={3}
78
+ isEdited
79
+ actions={<button onClick={onReply}><MessageReplyIcon /> Responder</button>}
80
+ />
81
+
82
+ <MessageBubble
83
+ side="received"
84
+ mediaType="image"
85
+ mediaUrl="/uploads/nota.jpg"
86
+ body="Comprovante"
87
+ createdAt="14:40"
88
+ onMediaClick={() => openLightbox("/uploads/nota.jpg")}
89
+ quotedMessage={{ authorName: "João", body: "Pode mandar o comprovante?" }}
90
+ />
91
+
92
+ <MessageBubble
93
+ side="sent"
94
+ origin="ai"
95
+ body="Oi! Sou a Sol. Posso ajudar com a sua fatura?"
96
+ createdAt={new Date()}
97
+ ack={3}
98
+ actions={<button onClick={onReply}><MessageReplyIcon /> Responder</button>}
99
+ />
100
+ ```
101
+
102
+ ## Gotchas
103
+
104
+ - **`max-w-[70%]` é exceção de hardcode documentada** — não há token DS de porcentagem para largura de balão de chat; a coluna da bolha limita a 70% da linha por regra do domínio. Todo o resto é token DS.
105
+ - **Ack só em `sent`:** `MessageAck` nunca aparece em mensagens recebidas (regra WA), mesmo se `ack` for passado. `ackError` (encaminhado como `error` ao `MessageAck`) troca o glifo pelo ícone de erro de envio.
106
+ - **Mídia clicável é acessível por teclado:** quando `onMediaClick` é passado, a imagem/vídeo recebe `role="button"` + `tabIndex={0}` + ativação por Enter/Space e anel de foco DS. Sem `onMediaClick`, a mídia não é focável (apenas exibição).
107
+ - **Documento: o CARD INTEIRO é o alvo do clique**, não um ícone na borda. Com `onMediaClick` o card vira `<button>` (teclado e leitor de tela de graça) e o ícone de baixar é só decoração — botão dentro de botão seria HTML inválido. Sem `onMediaClick` ele fica inerte e o rótulo passa de "Toque para abrir" para "Documento recebido": componente dumb não promete gesto que ninguém ligou. Até 2026-08-20 só o ícone de ~16px tinha clique, e em coluna estreita ele fica fora da área visível — o card ensinava um gesto que não existia.
108
+ - **`isDeleted` vence tudo:** suprime corpo, mídia, citação e ações — só mostra "Mensagem apagada" em itálico mudo com ícone de proibido.
109
+ - **`media` slot = override:** quando passado, substitui o `MessageMediaRenderer` interno por completo (use para players custom, mapas embedados, etc).
110
+ - **Citação inline:** a prévia da citação usa `<MarkdownText inline>` (1 linha, truncável). Sem body, mostra o rótulo do tipo de mídia ("Imagem", "Áudio"…).
111
+ - **Ações via Popover não-modal:** o botão ⋮ usa `PopoverAnchor` (não Trigger) + `modal={false}` (L-022/L-031) para o toggle externo não conflitar com o hover; aparece no `group-hover` da bolha (e no foco por teclado).
112
+ - **O botão ⋮ tem superfície própria** (`bg-bg-emphasis` em pílula + `shadow-sh-sm`, glifo de 16px via `size="icon-xs"`). Sem isso ele sumia no tema escuro: bolha recebida (`bg-bg-surface`) e fundo da conversa têm quase a mesma luminância, e o ghost secundário só tinha `hover:bg-bg-muted` (3% de branco). Pelo mesmo motivo a bolha recebida tem `border-border-subtle` em vez de borda transparente.
113
+ - **`origin="ai"` é só a variante:** o componente não sabe quem escreveu. O consumidor aplica o mesmo critério que já usa para ligar `actions` só às mensagens da IA.
114
+ - **Subcomponente `MessageMediaRenderer` é interno** — não exportado. Consuma só via `MessageBubble`.
115
+ ```
@@ -0,0 +1,80 @@
1
+ # MessageComposer
2
+
3
+ **Categoria:** composto (chat / atendimento). Compõe `Textarea` + `Button` + `Icon` + `Separator`.
4
+
5
+ Shell DUMB (sem lógica de API/upload/emoji/áudio) da barra de envio de mensagem,
6
+ usado no Atendimento e no Chat interno. Só a moldura + slots; o consumer pluga
7
+ emoji, anexo, mic, quick-messages, gravação, etc.
8
+
9
+ ## Quando usar
10
+
11
+ - Caixa de digitação de qualquer conversa (ticket WhatsApp, chat interno).
12
+ - Sempre que precisar de textarea auto-grow + botão de envio + slots de toolbar,
13
+ citação e aviso, sem reimplementar a moldura.
14
+
15
+ ## Props essenciais
16
+
17
+ | Prop | Tipo | Default | Descrição |
18
+ |------|------|---------|-----------|
19
+ | `value` | `string` | — (req) | Texto controlado da mensagem. |
20
+ | `onChange` | `(v: string) => void` | — (req) | Disparado a cada digitação. |
21
+ | `onSend` | `() => void` | — (req) | Enter (sem Shift) ou clique no botão. Só dispara com texto e fora de disabled/sending. |
22
+ | `onKeyDown` | `(e) => void` | — | Roda ANTES do Enter→onSend interno; `e.preventDefault()` suprime o envio padrão. |
23
+ | `placeholder` | `string` | — | Placeholder da textarea (também vira `aria-label`). |
24
+ | `state` | `'open' \| 'disabled' \| 'read-only'` | `'open'` | `read-only` esconde o campo e mostra só o `banner`. |
25
+ | `size` | `'sm' \| 'md'` | `'md'` | Padding/altura do field e da textarea. |
26
+ | `toolbarStart` | `ReactNode` | — | Slot à esquerda (emoji/anexo). |
27
+ | `toolbarEnd` | `ReactNode` | — | Slot à direita, antes do envio (mic/extra). |
28
+ | `replyPreview` | `ReactNode` | — | Barra de citação acima da textarea. |
29
+ | `banner` | `ReactNode` | — | Aviso acima do campo (janela 24h / read-only). |
30
+ | `sending` | `boolean` | `false` | Botão de envio em loading + bloqueado. |
31
+ | `recording` | `ReactNode` | — | SUBSTITUI a textarea enquanto grava (waveform/timer). |
32
+ | `className` | `string` | — | className do container. |
33
+
34
+ `ref` é encaminhado para a `<textarea>` interna (foco programático).
35
+
36
+ ## Variants
37
+
38
+ | Variant | Valores | Efeito |
39
+ |---------|---------|--------|
40
+ | `state` | open · disabled · read-only | editável · bloqueado · sem campo (só banner) |
41
+ | `size` | sm · md | altura/padding compacto · padrão |
42
+
43
+ ## Exemplo mínimo
44
+
45
+ ```tsx
46
+ import { MessageComposer } from "@/components/ui/MessageComposer";
47
+ import { Button } from "@/components/ui/Button";
48
+ import { Icon } from "@/components/ui/Icon";
49
+
50
+ const [text, setText] = useState("");
51
+
52
+ <MessageComposer
53
+ value={text}
54
+ onChange={setText}
55
+ onSend={() => {
56
+ sendMessage(text);
57
+ setText("");
58
+ }}
59
+ placeholder="Digite uma mensagem..."
60
+ toolbarStart={
61
+ <Button variant="ghost" color="secondary" size="icon-sm" aria-label="Anexar">
62
+ <Icon name="line-file-attachment" />
63
+ </Button>
64
+ }
65
+ />
66
+ ```
67
+
68
+ ## Gotchas / cuidados
69
+
70
+ - **DUMB por design:** emoji picker, upload, gravação de áudio e quick-messages são
71
+ responsabilidade do consumer (slots). O composer não tem estado de negócio.
72
+ - **Enter→onSend** é embutido. Para desabilitar (ex.: textarea multiline puro), trate
73
+ no `onKeyDown` e chame `e.preventDefault()`.
74
+ - **Auto-grow** cresce até um teto (`max-h` interno) e então rola; a altura é
75
+ recalculada via `useLayoutEffect` a cada mudança de `value`. Componentes controlados.
76
+ - **Botão de envio** usa o ícone `line-telegram` e só habilita com `value.trim()`.
77
+ Em `sending` entra em loading e bloqueia.
78
+ - **`recording`** remove a textarea E o botão de envio padrão — o consumer controla as
79
+ ações de gravação dentro desse slot (cancelar/enviar áudio).
80
+ - Ícone de anexo/emoji/mic não vem embutido: passe Buttons ghost com `Icon` nos slots.
@@ -0,0 +1,104 @@
1
+ # MessageVariablesPicker
2
+
3
+ **Categoria:** composto (ui/) — picker de variáveis de mensagem.
4
+
5
+ Picker de variáveis `{{...}}` que emite `onSelect(token)`. Composto a partir de `Popover` (mobileSheet), `Button`, `Chip`, `Icon` e `Separator` do DS.
6
+
7
+ > ⚠️ **Não contém o textarea.** O componente só dispara `onSelect(token)` — quem insere o token no cursor é o consumer (campo de texto/editor).
8
+
9
+ ---
10
+
11
+ ## Quando usar
12
+
13
+ - Editores de mensagem (quick messages, campanhas, respostas automáticas) onde o usuário precisa inserir variáveis dinâmicas (`{{firstName}}`, `{{protocol}}`, etc).
14
+ - Ao lado/dentro da toolbar de um `<textarea>` — o trigger só-ícone cabe numa barra de ações.
15
+
16
+ ---
17
+
18
+ ## Props essenciais
19
+
20
+ | Prop | Tipo | Default | Descrição |
21
+ |------|------|---------|-----------|
22
+ | `onSelect` | `(token: string) => void` | — (req) | Recebe o token escolhido. O consumer insere no cursor. |
23
+ | `variables` | `MessageVariable[]` | `DEFAULT_MESSAGE_VARIABLES` | Lista exibida (`{ token, label }`). |
24
+ | `label` | `string` | `"Variáveis"` | Rótulo do trigger (`triggerVariant="button"`). |
25
+ | `triggerVariant` | `"icon" \| "button"` | `"icon"` | `icon` = só-ícone (glyph chaves) · `button` = ícone + label. |
26
+ | `open` / `onOpenChange` | `boolean` / `(o)=>void` | — | Controle externo de abertura (opcional). |
27
+ | `closeOnSelect` | `boolean` | `true` | Fecha o popover ao selecionar. |
28
+ | `size` | `"sm" \| "md"` | `"sm"` | Tamanho do trigger e largura mínima do conteúdo. |
29
+ | `disabled` | `boolean` | — | Desabilita o trigger. |
30
+ | `className` | `string` | — | Classe extra no trigger. |
31
+
32
+ ### `MessageVariable`
33
+
34
+ ```ts
35
+ type MessageVariable = { token: string; label: string };
36
+ ```
37
+
38
+ ### `DEFAULT_MESSAGE_VARIABLES`
39
+
40
+ ```ts
41
+ [
42
+ { token: "{{firstName}}", label: "Primeiro Nome" },
43
+ { token: "{{name}} ", label: "Nome" },
44
+ { token: "{{ms}} ", label: "Saudação" },
45
+ { token: "{{protocol}} ", label: "Protocolo" },
46
+ { token: "{{hora}} ", label: "Hora" },
47
+ ]
48
+ ```
49
+
50
+ > Os trailing spaces são **propositais** (token concatenado direto no texto) — todos têm espaço final **exceto** `{{firstName}}`.
51
+
52
+ ---
53
+
54
+ ## Variants
55
+
56
+ | Variant | Valores | Default |
57
+ |---------|---------|---------|
58
+ | `triggerVariant` | `icon`, `button` | `icon` |
59
+ | `size` | `sm`, `md` | `sm` |
60
+
61
+ ---
62
+
63
+ ## Exemplo mínimo
64
+
65
+ ```tsx
66
+ import { MessageVariablesPicker } from "@/components/ui/MessageVariablesPicker";
67
+
68
+ function MessageEditor() {
69
+ const ref = useRef<HTMLTextAreaElement>(null);
70
+
71
+ function insertAtCursor(token: string) {
72
+ const el = ref.current;
73
+ if (!el) return;
74
+ const { selectionStart: s, selectionEnd: e, value } = el;
75
+ el.value = value.slice(0, s) + token + value.slice(e);
76
+ const pos = s + token.length;
77
+ el.setSelectionRange(pos, pos);
78
+ el.focus();
79
+ }
80
+
81
+ return (
82
+ <div className="flex items-end gap-gp-sm">
83
+ <textarea ref={ref} />
84
+ <MessageVariablesPicker onSelect={insertAtCursor} />
85
+ </div>
86
+ );
87
+ }
88
+ ```
89
+
90
+ Variante com label:
91
+
92
+ ```tsx
93
+ <MessageVariablesPicker triggerVariant="button" onSelect={insertAtCursor} />
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Gotchas / cuidados
99
+
100
+ - **Sem textarea embutido.** O componente só emite `onSelect`. Se você não inserir no cursor, nada acontece visualmente no campo.
101
+ - **Foco do textarea.** Os Chips usam `onMouseDown` + `preventDefault()` para não roubar o foco do campo antes do clique — preserve isso ao customizar.
102
+ - **Trailing spaces.** Não normalize os tokens de `DEFAULT_MESSAGE_VARIABLES`; o espaço final é parte do contrato.
103
+ - **A11y.** Trigger só-ícone tem `aria-label="Inserir variável"` + `aria-haspopup="dialog"`; cada Chip tem `aria-label="Inserir <label>"`.
104
+ - **Mobile.** Em telas `<md` o popover vira bottom-sheet (via `mobileSheet` do Popover).
@@ -0,0 +1,88 @@
1
+ # Modal — USAGE
2
+
3
+ <!-- ds:regras
4
+ - aba dentro dele → `<Tabs fullWidth>` até o size `lg`; no `xl` (1100px) use hug
5
+ - ação destrutiva → `AlertModal`, não confirmação montada na mão
6
+ -->
7
+
8
+ Dialog modal centrado com header (icon + title + description), body livre via `children` e footer estruturado por props de action (até 3 botões) — não há subcomponentes de composição.
9
+
10
+ ## Quando usar
11
+ - Confirmação ou input que **interrompe** o fluxo (ex: editar dados, formulário curto)
12
+ - Ação destrutiva → preferir `<AlertModal>` (mais explícito sobre tone)
13
+ - Side-drawer → preferir `<Panel>` ou `<FloatingPanel>`
14
+
15
+ ## Import
16
+ ```tsx
17
+ import { Modal } from "@/components/ui/Modal";
18
+ // types: import type { ModalProps, ModalAction, ModalSize } from "@/components/ui/Modal";
19
+ ```
20
+
21
+ ## Variants
22
+ | Variant | Valores | Default | Tamanho |
23
+ |---|---|---|---|
24
+ | `size` | sm / md / lg / xl / full | md | 440 / 540 / 720 / 1100px / min(1400px, 92vw) max-width |
25
+
26
+ O layout do footer NÃO é prop: com `tertiaryAction` presente o footer vira
27
+ `justify-between` (tertiary à esquerda, secondary+primary à direita);
28
+ sem tertiary, tudo fica à direita (`justify-end`).
29
+
30
+ ## Props essenciais
31
+ | Prop | Tipo | Default | Função |
32
+ |---|---|---|---|
33
+ | `open` | boolean | — | Controlled open (obrigatória) |
34
+ | `onClose` | () => void | — | Callback quando o modal pede pra fechar — X, ESC, overlay click ou ação (obrigatória) |
35
+ | `title` | ReactNode | — | Header title |
36
+ | `description` | ReactNode | — | Header subtitle (opcional) |
37
+ | `icon` | ReactNode | — | Icon decorativo no header (container 40×40) |
38
+ | `children` | ReactNode | — | Conteúdo do body (flex-col, gap 18px) |
39
+ | `primaryAction` | ModalAction | — | Botão filled à direita (critical quando `danger: true`) |
40
+ | `secondaryAction` | ModalAction | — | Botão outline secondary. `onClick` default = `onClose` |
41
+ | `tertiaryAction` | ModalAction | — | Botão ghost à ESQUERDA (ativa justify-between). Ghost critical quando `danger: true` |
42
+ | `footer` | ReactNode | — | Escape hatch — substitui inteiramente as 3 actions estruturadas |
43
+ | `size` | "sm" \| "md" \| "lg" \| "xl" \| "full" | "md" | Largura máxima (xl = 1100px p/ modais de dados; full = min(1400px, 92vw)) |
44
+ | `hideClose` | boolean | false | Esconde o X de fechar |
45
+ | `closeOnOverlay` | boolean | true | Click no overlay fecha o modal |
46
+
47
+ Shape de `ModalAction`:
48
+ ```ts
49
+ type ModalAction = {
50
+ label: ReactNode;
51
+ onClick?: () => void;
52
+ disabled?: boolean;
53
+ loading?: boolean;
54
+ danger?: boolean; // pinta o botão com cor critical (destrutivo)
55
+ };
56
+ ```
57
+
58
+ ## Exemplo mínimo
59
+ ```tsx
60
+ <Modal
61
+ open={open}
62
+ onClose={() => setOpen(false)}
63
+ size="md"
64
+ title="Editar cliente"
65
+ description="Alterações são salvas automaticamente"
66
+ primaryAction={{ label: "Salvar", onClick: handleSave }}
67
+ secondaryAction={{ label: "Cancelar" }}
68
+ >
69
+ <FormFields />
70
+ </Modal>
71
+ ```
72
+
73
+ ## Cuidados / Gotchas
74
+ - **Aba dentro do Modal** → `<Tabs fullWidth>` com a variante **default** (`segmented`) até o size `lg` (720px). No `xl` (1100px) **não** use `fullWidth`: cada aba fica enorme e o grupo passa a ler como barra de segmented control, não como aba.
75
+
76
+ - Footer aparece só quando há `footer` OU alguma action estruturada — sem nada disso, o modal não renderiza footer
77
+ - `tertiaryAction` à esquerda é o padrão pra ações destrutivas (ex: "Deletar") ou neutras (ajuda); o grupo cancel/save fica à direita
78
+ - **Body scrolla sozinho — não faça na mão.** O painel tem teto de altura
79
+ (`max-h-[calc(100dvh-32px)]`, `dvh` por causa da barra do navegador no mobile) e o body é
80
+ `min-h-0 flex-1 overflow-y-auto`. Conteúdo longo (form grande, lista) rola dentro do
81
+ modal, com header e footer fixos. O `min-h-0` é o que permite o flex item encolher — sem
82
+ ele o `overflow-y-auto` não tem efeito nenhum e o conteúdo empurra o container pra fora
83
+ da tela. Não replique altura/scroll no `children`: vira scroll duplo.
84
+ <br>Até v0.30.0 o body **não** scrollava (`overflow-hidden` clipava e não se chegava aos
85
+ botões do footer num form longo); corrigido em v0.30.1.
86
+ - `closeOnOverlay={false}` ignora fechamento por overlay E por ESC (Radix dispara o mesmo callback) — pra prevenção fina, controle externamente
87
+ - Render via portal — escapa de overflow/transform ancestrais
88
+ - Pra confirmação destrutiva, use `<AlertModal>` (tem `tone="danger"` semântico)
@@ -0,0 +1,49 @@
1
+ # MonthYearPicker
2
+
3
+ **Categoria:** composto (Popover + grade de meses). Seletor de período **mês+ano**.
4
+
5
+ ## Quando usar
6
+
7
+ - Filtro de período por competência mensal (ex.: "Ranking Verticais" por mês), relatórios mensais, faturas.
8
+ - Quando um `calendar` de dia é granular demais — você só quer `mês/ano`.
9
+
10
+ O valor é sempre `"YYYY-MM"` (ex.: `"2026-07"`).
11
+
12
+ ## Props essenciais
13
+
14
+ | Prop | Tipo | Default | Descrição |
15
+ |------|------|---------|-----------|
16
+ | `value` | `string` (`"YYYY-MM"`) | — | Período selecionado (controlado). |
17
+ | `onValueChange` | `(value: string) => void` | — | Recebe `"YYYY-MM"` ao escolher um mês. |
18
+ | `placeholder` | `ReactNode` | `"Selecione o mês"` | Texto do trigger vazio. |
19
+ | `min` / `max` | `string` (`"YYYY-MM"`) | — | Faixa selecionável (inclusive). Fora dela, meses ficam desabilitados. |
20
+ | `locale` | `string` | `"pt-BR"` | Locale dos rótulos de mês (via `Intl`). |
21
+ | `align` | `"start" \| "center" \| "end"` | `"start"` | Alinhamento do dropdown. |
22
+ | `open` / `defaultOpen` / `onOpenChange` | — | — | Controle de abertura (igual a um Select). |
23
+ | `disabled` | `boolean` | — | Desabilita o trigger. |
24
+ | `className` | `string` | — | Estiliza o **trigger** (aceita os mesmos overrides de um `SelectTrigger`). |
25
+ | `contentClassName` | `string` | — | Estiliza o **dropdown**. |
26
+
27
+ ## Exemplo mínimo
28
+
29
+ ```tsx
30
+ import { MonthYearPicker } from "@snksergio/design-system";
31
+
32
+ const [periodo, setPeriodo] = useState("2026-07");
33
+
34
+ <MonthYearPicker
35
+ value={periodo}
36
+ onValueChange={setPeriodo}
37
+ min="2023-01"
38
+ max="2026-12"
39
+ aria-label="Período do ranking"
40
+ />
41
+ // trigger mostra "Julho de 2026"; abre grade Jan…Dez com ‹ 2026 ›
42
+ ```
43
+
44
+ ## Gotchas
45
+
46
+ - **Formato fixo `"YYYY-MM"`** — mês com zero à esquerda (`"2026-01"`, não `"2026-1"`). Valores fora desse formato são tratados como "nada selecionado".
47
+ - O trigger espelha o `SelectTrigger` (mesma altura/borda/foco). Para parear com Selects irmãos num form, passe o mesmo `className`.
48
+ - Rótulos vêm do `Intl` no `locale`: pt-BR gera "Jan", "Fev"… e "Julho de 2026" no trigger (primeira letra capitalizada pelo componente).
49
+ - Navegação de ano (`‹`/`›`) só desabilita quando o ano inteiro cai fora de `[min, max]`; dentro do ano, meses individuais é que desabilitam.