@snksergio/design-system 0.62.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 (135) 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/exemplos/app-shell/app-shell-example.tsx +140 -0
  7. package/dist-lib/ai/exemplos/app-shell/index.ts +3 -0
  8. package/dist-lib/ai/exemplos/app-shell/nav-data.ts +97 -0
  9. package/dist-lib/ai/exemplos/app-shell/routes.tsx +75 -0
  10. package/dist-lib/ai/exemplos/chat/chat-screen.tsx +152 -0
  11. package/dist-lib/ai/exemplos/chat/chat-v2-mocks.ts +171 -0
  12. package/dist-lib/ai/exemplos/chat/chat-v2.styles.ts +23 -0
  13. package/dist-lib/ai/exemplos/chat/chat-v2.types.ts +81 -0
  14. package/dist-lib/ai/exemplos/chat/components/ChannelDot/channel-dot.tsx +28 -0
  15. package/dist-lib/ai/exemplos/chat/components/ConversationActionsMenu/conversation-actions-menu.tsx +83 -0
  16. package/dist-lib/ai/exemplos/chat/components/ConversationActionsMenu/index.ts +4 -0
  17. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.styles.ts +22 -0
  18. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.tsx +203 -0
  19. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.types.ts +10 -0
  20. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/index.ts +2 -0
  21. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.styles.ts +55 -0
  22. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.tsx +52 -0
  23. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.types.ts +7 -0
  24. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/index.ts +2 -0
  25. package/dist-lib/ai/exemplos/chat/components/DateSeparator/date-separator.tsx +18 -0
  26. package/dist-lib/ai/exemplos/chat/components/DetailField/detail-field.tsx +23 -0
  27. package/dist-lib/ai/exemplos/chat/components/DetailSection/detail-section.tsx +41 -0
  28. package/dist-lib/ai/exemplos/chat/components/DetailSection/index.ts +1 -0
  29. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.styles.ts +30 -0
  30. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.tsx +142 -0
  31. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.types.ts +16 -0
  32. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/index.ts +5 -0
  33. package/dist-lib/ai/exemplos/chat/components/FilterRow/filter-row.styles.ts +37 -0
  34. package/dist-lib/ai/exemplos/chat/components/FilterRow/filter-row.tsx +45 -0
  35. package/dist-lib/ai/exemplos/chat/components/FilterRow/index.ts +1 -0
  36. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.styles.ts +27 -0
  37. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.tsx +66 -0
  38. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.types.ts +13 -0
  39. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-rail.tsx +38 -0
  40. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/index.ts +6 -0
  41. package/dist-lib/ai/exemplos/chat/components/MessageBubble/index.ts +2 -0
  42. package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.styles.ts +37 -0
  43. package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.tsx +29 -0
  44. package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.types.ts +5 -0
  45. package/dist-lib/ai/exemplos/chat/components/PersonAvatar/index.ts +5 -0
  46. package/dist-lib/ai/exemplos/chat/components/PersonAvatar/person-avatar.tsx +31 -0
  47. package/dist-lib/ai/exemplos/chat/components/QueueColumn/index.ts +2 -0
  48. package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.styles.ts +19 -0
  49. package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.tsx +140 -0
  50. package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.types.ts +13 -0
  51. package/dist-lib/ai/exemplos/chat/components/RailItem/index.ts +1 -0
  52. package/dist-lib/ai/exemplos/chat/components/RailItem/rail-item.styles.ts +30 -0
  53. package/dist-lib/ai/exemplos/chat/components/RailItem/rail-item.tsx +42 -0
  54. package/dist-lib/ai/exemplos/chat/hooks/use-resizable.ts +98 -0
  55. package/dist-lib/ai/exemplos/chat/index.ts +1 -0
  56. package/dist-lib/ai/exemplos/clientes/_table-data.ts +59 -0
  57. package/dist-lib/ai/exemplos/clientes/clientes-screen.tsx +505 -0
  58. package/dist-lib/ai/exemplos/clientes/clientes-showcase-mocks.ts +117 -0
  59. package/dist-lib/ai/exemplos/clientes/clientes-showcase.styles.ts +13 -0
  60. package/dist-lib/ai/exemplos/clientes/clientes-showcase.types.ts +16 -0
  61. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.styles.ts +28 -0
  62. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.tsx +303 -0
  63. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.types.ts +14 -0
  64. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/index.ts +2 -0
  65. package/dist-lib/ai/exemplos/clientes/components/NovoClienteDrawer/index.ts +2 -0
  66. package/dist-lib/ai/exemplos/clientes/components/NovoClienteDrawer/novo-cliente-drawer.tsx +207 -0
  67. package/dist-lib/ai/exemplos/clientes/index.ts +1 -0
  68. package/dist-lib/ai/exemplos/edit-page/components/StepNav.tsx +81 -0
  69. package/dist-lib/ai/exemplos/edit-page/components/section-card.tsx +85 -0
  70. package/dist-lib/ai/exemplos/edit-page/edit-page-screen.tsx +234 -0
  71. package/dist-lib/ai/exemplos/edit-page/index.ts +1 -0
  72. package/dist-lib/ai/exemplos/finance/_table-data.ts +57 -0
  73. package/dist-lib/ai/exemplos/finance/clientes-financeiro-mocks.ts +227 -0
  74. package/dist-lib/ai/exemplos/finance/clientes-financeiro.types.ts +75 -0
  75. package/dist-lib/ai/exemplos/finance/clientes-showcase-mocks.ts +117 -0
  76. package/dist-lib/ai/exemplos/finance/clientes-showcase.styles.ts +13 -0
  77. package/dist-lib/ai/exemplos/finance/clientes-showcase.types.ts +16 -0
  78. package/dist-lib/ai/exemplos/finance/components/EditarFinanceDrawer/editar-finance-drawer.tsx +241 -0
  79. package/dist-lib/ai/exemplos/finance/components/EditarFinanceDrawer/index.ts +5 -0
  80. package/dist-lib/ai/exemplos/finance/components/ExtratoExpansion/extrato-expansion.tsx +172 -0
  81. package/dist-lib/ai/exemplos/finance/components/ExtratoExpansion/index.ts +1 -0
  82. package/dist-lib/ai/exemplos/finance/components/FinanceDetailPanel/finance-detail-panel.tsx +241 -0
  83. package/dist-lib/ai/exemplos/finance/components/FinanceDetailPanel/index.ts +2 -0
  84. package/dist-lib/ai/exemplos/finance/components/NovoClienteDrawer/index.ts +2 -0
  85. package/dist-lib/ai/exemplos/finance/components/NovoClienteDrawer/novo-cliente-drawer.tsx +207 -0
  86. package/dist-lib/ai/exemplos/finance/components/SacarDialog/index.ts +2 -0
  87. package/dist-lib/ai/exemplos/finance/components/SacarDialog/sacar-dialog.tsx +346 -0
  88. package/dist-lib/ai/exemplos/finance/finance-screen.tsx +821 -0
  89. package/dist-lib/ai/exemplos/finance/index.ts +1 -0
  90. package/dist-lib/ai/exemplos/gantt/_gantt-data.tsx +402 -0
  91. package/dist-lib/ai/exemplos/gantt/gantt-screen.tsx +512 -0
  92. package/dist-lib/ai/exemplos/gantt/index.ts +1 -0
  93. package/dist-lib/ai/exemplos/login/index.ts +1 -0
  94. package/dist-lib/ai/exemplos/login/login-screen.tsx +246 -0
  95. package/dist-lib/ai/exemplos/mapa-rede/components/ConsultorDetailPanel/consultor-detail-panel.tsx +158 -0
  96. package/dist-lib/ai/exemplos/mapa-rede/components/ConsultorDetailPanel/index.ts +2 -0
  97. package/dist-lib/ai/exemplos/mapa-rede/index.ts +1 -0
  98. package/dist-lib/ai/exemplos/mapa-rede/mapa-de-rede-mocks.ts +655 -0
  99. package/dist-lib/ai/exemplos/mapa-rede/mapa-de-rede.types.ts +53 -0
  100. package/dist-lib/ai/exemplos/mapa-rede/mapa-rede-screen.tsx +227 -0
  101. package/dist-lib/ai/exemplos/order-detail/components/ActivityTab.tsx +77 -0
  102. package/dist-lib/ai/exemplos/order-detail/components/AttachmentsTab.tsx +45 -0
  103. package/dist-lib/ai/exemplos/order-detail/components/CommentsTab.tsx +111 -0
  104. package/dist-lib/ai/exemplos/order-detail/components/DetailsTab.tsx +189 -0
  105. package/dist-lib/ai/exemplos/order-detail/components/OverviewTab.tsx +200 -0
  106. package/dist-lib/ai/exemplos/order-detail/components/section-card.tsx +85 -0
  107. package/dist-lib/ai/exemplos/order-detail/index.ts +1 -0
  108. package/dist-lib/ai/exemplos/order-detail/order-detail-screen.tsx +119 -0
  109. package/dist-lib/ai/exemplos/order-detail/order-mocks.ts +186 -0
  110. package/dist-lib/ai/exemplos/order-detail/order.types.ts +118 -0
  111. package/dist-lib/ai/indice.json +30 -15
  112. package/dist-lib/ai/manifest.json +31 -5
  113. package/dist-lib/ai/roteiros/app-builder/roteiro.md +155 -0
  114. package/dist-lib/ai/roteiros/auth-builder/roteiro.md +42 -0
  115. package/dist-lib/ai/roteiros/cards/roteiro.md +34 -0
  116. package/dist-lib/ai/roteiros/charts/roteiro.md +32 -0
  117. package/dist-lib/ai/roteiros/chat/roteiro.md +31 -0
  118. package/dist-lib/ai/roteiros/crud-builder/blueprint.md +63 -0
  119. package/dist-lib/ai/roteiros/crud-builder/entrevista.md +139 -0
  120. package/dist-lib/ai/roteiros/crud-builder/geracao.md +113 -0
  121. package/dist-lib/ai/roteiros/crud-builder/roteiro.md +89 -0
  122. package/dist-lib/ai/roteiros/drawers/roteiro.md +41 -0
  123. package/dist-lib/ai/roteiros/list-builder/blueprint.md +94 -0
  124. package/dist-lib/ai/roteiros/list-builder/entrevista.md +188 -0
  125. package/dist-lib/ai/roteiros/list-builder/geracao.md +63 -0
  126. package/dist-lib/ai/roteiros/list-builder/roteiro.md +91 -0
  127. package/dist-lib/ai/roteiros/module-replicator/roteiro.md +56 -0
  128. package/dist-lib/ai/roteiros/page-detail/roteiro.md +32 -0
  129. package/dist-lib/ai/roteiros/page-edit/roteiro.md +31 -0
  130. package/dist-lib/ai/roteiros/screen-composer/roteiro.md +95 -0
  131. package/package.json +1 -1
  132. /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/blueprint.md +0 -0
  133. /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/entrevista.md +0 -0
  134. /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/geracao.md +0 -0
  135. /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/roteiro.md +0 -0
@@ -0,0 +1,555 @@
1
+ import { useState } from "react";
2
+ import { Check, Circle, MessageCircle, Pencil, PiggyBank, Receipt } from "lucide-react";
3
+ import type { LucideIcon } from "@/lib/lucide-types";
4
+ import { Avatar } from "@/components/ui/avatar-ig";
5
+ import { Button } from "@/components/ui/Button";
6
+ import { Chip } from "@/components/ui/Chip";
7
+ import {
8
+ FloatingPanel,
9
+ FloatingPanelField,
10
+ FloatingPanelSection,
11
+ } from "@/components/ui/FloatingPanel";
12
+
13
+ export const BLOCK = {
14
+ id: "dsgreen-paneldetail-1",
15
+ nome: "Painel de detalhe do registro",
16
+ descricao:
17
+ "Painel lateral aberto a partir de uma linha de tabela: header com avatar + nome + código e status, ações de ícone e maximizar; métricas em cards compactos; e os dados em seções colapsáveis — inclusive as que não são label:valor (conta bancária com marca, métodos em chips, e-mail e telefone acionáveis, gestor com avatar). Ação primária no footer. Mesmo padrão dos painéis de detalhe do Virtual Office e do CRUD de clientes.",
18
+ usa: [
19
+ "FloatingPanel (side=\"right\", size=\"lg\", titleSlot + headerActions + maximizable + resizable + bodyPadded={false})",
20
+ "FloatingPanelSection + FloatingPanelField",
21
+ "MetricaCartoes — cards compactos próprios (ícone + valor + rótulo); NÃO o Kpi",
22
+ "Avatar (colorHex, contraste WCAG automático)",
23
+ "Chip (soft) · Button (icon-sm no header, sm no footer)",
24
+ ],
25
+ } as const;
26
+
27
+ /**
28
+ * dsgreen-paneldetail-1 — painel de detalhe de um registro, aberto da linha da tabela.
29
+ *
30
+ * ## O que este bloco resolve
31
+ *
32
+ * Pedindo "abre um painel com o detalhe do cliente", sai uma pilha de `label: valor` —
33
+ * correta e ilegível a partir do 12º campo. O que falta não é componente, é **ordem**, e
34
+ * **onde cada coisa mora**: o painel tem header, corpo e footer com papéis distintos, e
35
+ * jogar tudo no corpo é o erro que faz o resultado parecer improvisado.
36
+ *
37
+ * | zona | o que vai | por que |
38
+ * |---|---|---|
39
+ * | **header** (`titleSlot`) | avatar + nome + código · status | responde "de quem é este painel", e fica fixo no scroll |
40
+ * | **header** (`headerActions`) | ações de ícone (editar, contatar) | ações frequentes e sem rótulo, à mão sem rolar |
41
+ * | corpo, 1ª faixa | **métricas** do registro | responde "como este registro está?", que é a pergunta de quem abriu — vem antes dos campos, que respondem "quais são os dados dele" |
42
+ * | corpo, resto | campos em **seções colapsáveis** por assunto | é o colapso que permite ter 20 campos sem obrigar a rolar 20 |
43
+ * | **footer** | Fechar + ação primária | a ação que fecha a tarefa, sempre alcançável (footer é sticky) |
44
+ *
45
+ * **Não há abas.** Elas estavam aqui e saíram: aba esconde conteúdo, e num painel onde o corpo
46
+ * já é uma pilha de seções colapsáveis, o colapso **é** o mecanismo de esconder. Ter os dois é
47
+ * dizer a mesma coisa de duas formas, e o usuário passa a ter que descobrir em qual das duas o
48
+ * dado está. Se um recorte for volumoso de verdade (extrato com 200 linhas), ele não é seção
49
+ * nem aba: é outra tela.
50
+ *
51
+ * ## Por que `FloatingPanel` e não `Panel` — foi o HEADER que decidiu
52
+ *
53
+ * As duas primeiras versões deste bloco usaram `Panel`, e o header saía fora do padrão dos
54
+ * painéis reais. A causa é de API, não de estilo: o `Panel` aceita `title` e `description`
55
+ * como **string**, e o header dos painéis de referência tem avatar, `Chip` de status inline e
56
+ * botões de ícone — nada disso cabe numa string. O `FloatingPanel` expõe exatamente isso:
57
+ *
58
+ * | o que o header precisa | prop |
59
+ * |---|---|
60
+ * | avatar + nome + código · status | `titleSlot` (JSX, substitui title/description) |
61
+ * | 1–2 ações de ícone | `headerActions` (entram à esquerda do maximize/close) |
62
+ * | expandir | `maximizable` — o botão é do componente, não se escreve |
63
+ * | fechar | nativo (o X sempre está lá; `hideClose` remove) |
64
+ *
65
+ * **Bônus:** `bodyPadded={false}` resolveu, com uma prop, o `-mx-pad-3xl` que a versão em
66
+ * `Panel` precisava pra fazer a divisória das seções chegar na borda. A prop existe justamente
67
+ * pra este caso — seção de detalhe é edge-to-edge por desenho.
68
+ *
69
+ * ### E as outras duas cascas
70
+ *
71
+ * `Panel`, `FloatingPanel` e `Drawer` são todos superfície lateral; a diferença é
72
+ * **comportamento** — o corpo é o mesmo nos três:
73
+ *
74
+ * | casca | comportamento | quando |
75
+ * |---|---|---|
76
+ * | **`FloatingPanel`** (esta) | non-modal, resizável, maximizável, sem backdrop | detalhe que coexiste com a lista atrás: o usuário compara, ou percorre linha por linha |
77
+ * | `Panel` | modal, com backdrop | o detalhe é a tarefa e a lista pode esperar. ⚠️ header só com strings |
78
+ * | `Drawer` | bottom-sheet (vaul) | mobile-first, ou gesto de arrastar |
79
+ *
80
+ * ## Regras do DS que este bloco carrega (e que copiar sem elas quebra)
81
+ *
82
+ * - **`bodyPadded={false}` é obrigatório com `FloatingPanelSection`.** É regra declarada no
83
+ * `USAGE.md` do componente: a seção traz o próprio padding de 18px e uma divisória de ponta
84
+ * a ponta; com o body padded o padding soma e a divisória para longe da borda.
85
+ * - **Campo que não é `label: valor` sai do `FloatingPanelField`.** Três casos aqui, e cada um
86
+ * tem um motivo medido: a **conta bancária** é entidade (marca + nome + agência/conta) e
87
+ * comprimida na coluna da direita ela trunca — vai numa linha de largura cheia; os
88
+ * **métodos** são itens discretos e viram `Chip`, não texto com vírgula; **e-mail e
89
+ * telefone** são ações (`mailto:`/`tel:`) e por isso levam cor de link.
90
+ * - **`Avatar` com `colorHex` escolhe a cor do texto por contraste WCAG** (L-027). `#820AD1`
91
+ * do Nubank dá branco (6.2 : 1); `#FAE128` do BB daria preto (16.3 : 1). Nunca escreva
92
+ * `text-white` na unha num avatar de marca — em marca clara isso vira texto ilegível.
93
+ * - **Ação de ícone precisa de `aria-label`.** Botão `size="icon-sm"` não tem texto: sem o
94
+ * label o leitor de tela anuncia "button" e pronto.
95
+ * - **Toda ação do header é `variant="soft"`, nunca `ghost`.** O maximize e o close que o
96
+ * `FloatingPanel` renderiza são `soft`+`secondary`; uma ação `ghost` no meio da fileira fica
97
+ * sem container e lê como desabilitada ao lado das outras. O que diferencia é a **cor**
98
+ * (`secondary` neutro, `success` contato, `critical` destrutivo), não a ausência de fundo.
99
+ * - **Métrica em painel NÃO é o `Kpi` do DS** — ver o JSDoc do `MetricaCartoes`. O `Kpi` é card
100
+ * de dashboard: medido aqui, 172×144px por célula, três delas comendo a primeira dobra do
101
+ * painel antes de qualquer campo. No painel a métrica é contexto, não o assunto.
102
+ * - ⚠️ Se ainda assim usar `KpiGroup`: **`columns` é responsivo ao VIEWPORT, não ao
103
+ * container** (`grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`). Num painel de 560px em desktop,
104
+ * `columns={3}` continua dando 3 colunas — a estreiteza do painel é invisível pro grid.
105
+ * - **A métrica também é seção colapsável.** Uma gramática só no corpo: métricas, campos e
106
+ * tudo mais entram como `FloatingPanelSection`. Faixa solta acima das seções criava uma
107
+ * segunda gramática visual no mesmo painel.
108
+ * - **Número com `tabular-nums`** — sem isso os dígitos dançam de um campo pro outro.
109
+ * - **Se um dia voltar a ter aba aqui: `fullWidth`, variante default (`segmented`).** Regra do
110
+ * `USAGE.md` do `Panel` e do `FloatingPanel` — em 560px o `line` vira um trilho curto que lê
111
+ * como fragmento. (Este bloco não tem aba; ver a nota acima.)
112
+ *
113
+ * ## Cuidado ao adaptar
114
+ *
115
+ * - **O botão daqui é do EXEMPLO.** No seu CRUD quem abre o painel é a linha selecionada da
116
+ * tabela: `open={!!linhaSelecionada}` e `onOpenChange={(o) => !o && limparSelecao()}`.
117
+ * - **`resizableStorageKey` só se o painel for recorrente.** Ele persiste a largura no
118
+ * `localStorage` — ótimo num painel que o usuário abre 50× por dia, ruído num painel raro.
119
+ * Use chave namespaced (`clientes.detail-panel.width`), senão dois painéis diferentes
120
+ * dividem a mesma largura.
121
+ * - **As seções são por ASSUNTO, e o assunto é do seu domínio.** Empresa / Conta bancária /
122
+ * Financeiro / Contato / Gestão é o recorte de um cliente de energia; num painel de pedido
123
+ * seria Itens / Entrega / Pagamento. Não herde estes títulos — herde o critério: cada seção
124
+ * responde uma pergunta, e campo que não responde a pergunta da seção está na seção errada.
125
+ * - **As métricas têm que ser do REGISTRO**, não da tela. "Total de clientes" não é métrica
126
+ * deste painel; "faturas em aberto deste cliente" é. Métrica de tela pertence ao dashboard
127
+ * ou ao topo da lista.
128
+ * - **`FloatingPanelField` cai pra `—` quando o valor é vazio** — não escreva "N/A" nem
129
+ * condicione a linha; passe o valor e deixe o componente resolver.
130
+ */
131
+
132
+ /** Fixture do bloco. Fica aqui de propósito: bloco é auto-contido, então quem copia vê a
133
+ * forma do dado esperado sem inferir do render. */
134
+ const REGISTRO = {
135
+ nome: "Padaria Estrela do Sul",
136
+ iniciais: "PE",
137
+ codigo: "CLI-2478",
138
+ status: "Ativo",
139
+ documento: "12.345.678/0001-90",
140
+ };
141
+
142
+ const METRICAS = [
143
+ { icone: PiggyBank, label: "Economia no ano", value: "R$ 3.180", tom: "brand" as const },
144
+ { icone: Receipt, label: "Em aberto", value: "R$ 892", tom: "danger" as const },
145
+ ];
146
+
147
+ const CONTA = {
148
+ banco: "Nubank",
149
+ iniciais: "NU",
150
+ hex: "#820AD1",
151
+ agencia: "3849",
152
+ conta: "18701-9",
153
+ };
154
+
155
+ const METODOS = ["PIX", "Boleto"];
156
+
157
+ const CONTATO = {
158
+ email: "contato@estreladosul.com.br",
159
+ telefone: "+55 31 98901-2345",
160
+ local: "Belo Horizonte, MG",
161
+ };
162
+
163
+ const GESTOR = { nome: "Aline Castro", iniciais: "AC", hex: "#CC092F" };
164
+
165
+ const CONSTRUCAO = {
166
+ situacao: "Em construção",
167
+ itens: [
168
+ { label: "Clientes do mês (green/telecom/seguro)", feitos: 3, total: 5 },
169
+ { label: "Conexão livre", feitos: 1, total: 1 },
170
+ { label: "Placa", feitos: 0, total: 1 },
171
+ { label: "Solar", feitos: 1, total: 1 },
172
+ { label: "Licenciados cadastrados", feitos: 1, total: 2 },
173
+ { label: "Club", feitos: 0, total: 1 },
174
+ ],
175
+ };
176
+
177
+ type MetricaItem = {
178
+ icone: LucideIcon;
179
+ label: string;
180
+ value: string;
181
+ tom: "brand" | "danger" | "neutro";
182
+ };
183
+
184
+ /** Cor do valor por tom. Fora do componente porque o tom é decisão de leitura, não de sinal. */
185
+ const COR_DO_TOM: Record<MetricaItem["tom"], string> = {
186
+ brand: "text-fg-brand",
187
+ danger: "text-fg-danger",
188
+ neutro: "text-fg-default",
189
+ };
190
+
191
+ /**
192
+ * **Métrica de painel — cards compactos, um por métrica.**
193
+ *
194
+ * ## Por que não é o `Kpi` do DS
195
+ *
196
+ * Não é rejeição do componente: é escala errada pro lugar. O `Kpi` é card de **dashboard** —
197
+ * `p-pad-3xl` (20px), label de 14px, slot pra sparkline, `hint` e `delta`. Num painel de
198
+ * detalhe a métrica é **contexto**, não o assunto: quem abriu quer os dados do registro, e a
199
+ * métrica só emoldura.
200
+ *
201
+ * Medido no browser, antes e depois:
202
+ *
203
+ * | | por item | conjunto |
204
+ * |---|---|---|
205
+ * | `Kpi size="sm"`, `columns={3}` | 172 × **144px** | 146px (3 métricas) |
206
+ * | este | 257 × **68px** | 68px (2 métricas) |
207
+ *
208
+ * ## Card próprio, e não uma faixa dividida
209
+ *
210
+ * A alternativa testada foi uma superfície única com `divide-x` (mesma altura, coluna 3px mais
211
+ * larga). A diferença entre as duas é **só o container**, e é isso que ela comunica: a faixa
212
+ * afirma que as métricas formam um conjunto; os cards afirmam que cada uma é independente.
213
+ * Economia e inadimplência são dois assuntos → cards. Total/pago/em aberto são três faces do
214
+ * mesmo número → faixa.
215
+ *
216
+ * ## Duas travas de layout
217
+ *
218
+ * 1. **O número de colunas segue o número de métricas** (`grid-cols-2` com duas). Não é
219
+ * responsivo de propósito: num painel de 560px a decisão é fixa. Acima de 3 não cabe, e a
220
+ * resposta é cortar métrica, não encolher coluna — em 4 colunas cada uma fica com ~127px e
221
+ * o par ícone + valor quebra linha.
222
+ * 2. **Ícone + valor na mesma linha depende do valor ser 18px.** Com `stat-sm` (20px) o par
223
+ * não cabia em 3 colunas e a 1ª versão saiu sem ícone; o ícone voltou junto com a redução.
224
+ * Se o valor crescer, **o ícone é a primeira coisa que quebra**.
225
+ *
226
+ * O 18px vem de `body-xl`, não de `stat-*`: o role `stat` começa em 20px (sm) e não tem tier
227
+ * abaixo. Com `font-bold` + `tabular-nums` a leitura de valor se mantém. Se precisar disso em
228
+ * mais lugares, o certo é um `stat-xs` no `typography.ts` via cascata — não repetir esta
229
+ * combinação por aí.
230
+ */
231
+ function MetricaCartoes({ itens }: { itens: MetricaItem[] }) {
232
+ return (
233
+ <div className="grid grid-cols-2 gap-gp-md">
234
+ {itens.map(({ icone: Icone, ...m }) => (
235
+ <div
236
+ key={m.label}
237
+ className="flex flex-col gap-gp-2xs rounded-radius-lg border border-border-default bg-bg-surface p-pad-2xl"
238
+ >
239
+ <div className={`flex items-center gap-gp-sm ${COR_DO_TOM[m.tom]}`}>
240
+ <Icone className="size-icon-sm shrink-0" aria-hidden="true" />
241
+ <span className="text-body-xl font-bold tabular-nums leading-none">{m.value}</span>
242
+ </div>
243
+ <span className="text-caption-sm text-fg-muted">{m.label}</span>
244
+ </div>
245
+ ))}
246
+ </div>
247
+ );
248
+ }
249
+
250
+ /**
251
+ * **Checklist de progresso** — a lista de metas do registro, com o que já fechou.
252
+ *
253
+ * ## O que ela resolve, e por que não é `Progress`
254
+ *
255
+ * O dado aqui não é "quanto por cento" — é **quais itens faltam**. Uma barra de `Progress`
256
+ * responde a primeira pergunta e apaga a segunda: 4 de 11 não diz que o que falta é Placa e
257
+ * Club. A lista mantém as duas leituras (o contador por item e o quadro geral) e é ela que
258
+ * permite agir.
259
+ *
260
+ * ## A faixa de situação é uma FAIXA, não um `Chip`
261
+ *
262
+ * `Chip` é rótulo inline — o que ele faz bem é caber ao lado de um texto. Aqui a situação é o
263
+ * cabeçalho do bloco: largura cheia, centralizada, `rounded-radius-full`. Forçar um Chip a
264
+ * `w-full` deforma um componente pra fazer o trabalho de outro. Tom em `warning` porque
265
+ * "em construção" é estado transitório com prazo, não erro nem sucesso.
266
+ *
267
+ * ## Estado não fica só no ícone (WCAG 1.4.1)
268
+ *
269
+ * O ✔ verde e o círculo vazio são **redundantes** — quem lê a informação é o contador
270
+ * `(1/1)` vs `(0/1)`, que é texto e é anunciado pelo leitor de tela. Por isso os ícones são
271
+ * `aria-hidden`. Se um dia o contador sair, o estado passa a viver só em cor + forma, e aí
272
+ * precisa de texto alternativo: **não remova o contador sem repor a informação.**
273
+ *
274
+ * ## Cuidado ao adaptar
275
+ *
276
+ * - `concluido` deriva de `feitos >= total`, não de um booleano à parte. Dois campos que podem
277
+ * discordar ("feito" true com 1 de 3) é bug esperando acontecer.
278
+ * - Lista longa (15+) some dentro do painel — aí ela não é seção, é tela própria com filtro.
279
+ */
280
+ function ListaDeProgresso({
281
+ situacao,
282
+ itens,
283
+ }: {
284
+ situacao: string;
285
+ itens: { label: string; feitos: number; total: number }[];
286
+ }) {
287
+ return (
288
+ <div className="flex flex-col gap-gp-lg">
289
+ <div className="rounded-radius-full bg-bg-warning-muted py-pad-md text-center text-body-sm font-semibold text-fg-warning">
290
+ {situacao}
291
+ </div>
292
+
293
+ <ul className="flex flex-col gap-gp-md">
294
+ {itens.map((i) => {
295
+ const concluido = i.feitos >= i.total;
296
+ return (
297
+ <li key={i.label} className="flex items-center gap-gp-md text-body-sm">
298
+ {/* Concluído = disco CHEIO, e ele é um `<span>` com `bg-bg-success`, não o
299
+ `CircleCheck` do lucide.
300
+
301
+ ⚠️ A tentativa anterior foi `<CircleCheck className="fill-bg-success
302
+ text-fg-on-success" />`, e o disco saía **visivelmente menor** que o anel do
303
+ pendente: o lucide desenha o círculo com `stroke="currentColor"`, então o
304
+ traço (preto no dark, porque `fg-on-success` inverte) fica POR CIMA do
305
+ preenchimento e come ~2px de verde de cada lado. Não há classe que tire só
306
+ aquele stroke — círculo e check compartilham o `currentColor`.
307
+
308
+ Com `<span>` + `<Check>` dentro, o disco é os 16px inteiros e o traço existe
309
+ só no check. `fg-on-success` é o par validado do `bg-success` em
310
+ `color-light/dark.ts` (branco no light, preto no dark) — não use `text-white`
311
+ na unha, que não é dark-aware.
312
+
313
+ O pendente fica de anel vazio de propósito: é o contraste cheio-vs-vazio que
314
+ carrega o estado. Preencher os dois apagaria a distinção. */}
315
+ {concluido ? (
316
+ <span
317
+ className="grid size-icon-sm shrink-0 place-items-center rounded-radius-full bg-bg-success text-fg-on-success"
318
+ aria-hidden="true"
319
+ >
320
+ <Check className="size-icon-2xs" strokeWidth={3} />
321
+ </span>
322
+ ) : (
323
+ <Circle className="size-icon-sm shrink-0 text-fg-subtle" aria-hidden="true" />
324
+ )}
325
+ <span
326
+ className={
327
+ concluido ? "font-semibold text-fg-default" : "text-fg-muted"
328
+ }
329
+ >
330
+ {i.label}{" "}
331
+ <span className="tabular-nums">
332
+ ({i.feitos}/{i.total})
333
+ </span>
334
+ </span>
335
+ </li>
336
+ );
337
+ })}
338
+ </ul>
339
+ </div>
340
+ );
341
+ }
342
+
343
+ /**
344
+ * Linha de seção que NÃO é `label: valor` — entidade com identidade própria.
345
+ *
346
+ * A conta bancária e o gestor não são um valor: são uma coisa com nome, marca e um dado
347
+ * secundário. Forçá-los no `FloatingPanelField` deixaria "Nubank · Ag 3849 · 18701-9"
348
+ * comprimido na coluna da direita e truncando. Aqui a linha usa a largura toda.
349
+ */
350
+ function LinhaDeEntidade({
351
+ hex,
352
+ iniciais,
353
+ nome,
354
+ detalhe,
355
+ }: {
356
+ hex: string;
357
+ iniciais: string;
358
+ nome: string;
359
+ detalhe: string;
360
+ }) {
361
+ return (
362
+ <div className="flex items-center gap-gp-lg">
363
+ <Avatar size="lg" colorHex={hex} className="shrink-0" aria-label={nome}>
364
+ {iniciais}
365
+ </Avatar>
366
+ <div className="flex min-w-0 flex-col">
367
+ <span className="truncate text-body-sm font-semibold text-fg-default">{nome}</span>
368
+ <span className="truncate text-body-xs tabular-nums text-fg-muted">{detalhe}</span>
369
+ </div>
370
+ </div>
371
+ );
372
+ }
373
+
374
+ export function PainelDeDetalheDoRegistro() {
375
+ const [aberto, setAberto] = useState(false);
376
+
377
+ return (
378
+ <>
379
+ {/* No CRUD real este botão não existe: quem abre o painel é a linha selecionada. */}
380
+ <Button variant="outline" color="secondary" onClick={() => setAberto(true)}>
381
+ Ver detalhe do cliente
382
+ </Button>
383
+
384
+ <FloatingPanel
385
+ open={aberto}
386
+ onOpenChange={setAberto}
387
+ side="right"
388
+ size="lg"
389
+ resizable
390
+ maximizable
391
+ bodyPadded={false}
392
+ resizableStorageKey="dsgreen-paneldetail-1.width"
393
+ /* Header: avatar + nome + (código · status). É o `titleSlot` que permite isso — com
394
+ `title`/`description` string não há como pôr avatar nem Chip. O `truncate` +
395
+ `min-w-0` são obrigatórios: nome longo empurraria as ações fora do painel. */
396
+ titleSlot={
397
+ <div className="flex min-w-0 items-center gap-gp-md">
398
+ <Avatar color="brand" size="lg" className="shrink-0" aria-label={REGISTRO.nome}>
399
+ {REGISTRO.iniciais}
400
+ </Avatar>
401
+ <div className="flex min-w-0 flex-col">
402
+ <span className="truncate text-body-md font-semibold text-fg-default">
403
+ {REGISTRO.nome}
404
+ </span>
405
+ <span className="mt-[2px] flex items-center gap-gp-sm text-body-xs text-fg-muted">
406
+ <span className="tabular-nums">{REGISTRO.codigo}</span>
407
+ <span className="opacity-50">·</span>
408
+ <Chip color="success" variant="soft" size="sm">
409
+ {REGISTRO.status}
410
+ </Chip>
411
+ </span>
412
+ </div>
413
+ </div>
414
+ }
415
+ /* Ações de ícone frequentes. O maximize e o X vêm do componente — não se escreve.
416
+ `aria-label` não é opcional aqui: botão de ícone não tem texto.
417
+
418
+ ⚠️ **`variant="soft"` em TODAS, não `ghost`.** O maximize e o close que o
419
+ `FloatingPanel` renderiza são `soft` + `secondary` — é o que dá a caixinha com fundo.
420
+ Uma ação `ghost` no meio da fileira fica sem container e lê como se estivesse
421
+ desabilitada ao lado das outras três. A cor é o que diferencia: `secondary` pro
422
+ neutro, `success` pra ação de contato. (O JSDoc do próprio componente mostra
423
+ `headerActions` com `soft`/`secondary` — foi de lá que a receita saiu.) */
424
+ headerActions={
425
+ <>
426
+ <Button variant="soft" color="secondary" size="icon-sm" aria-label="Editar cliente">
427
+ <Pencil />
428
+ </Button>
429
+ <Button variant="soft" color="success" size="icon-sm" aria-label="WhatsApp">
430
+ <MessageCircle />
431
+ </Button>
432
+ </>
433
+ }
434
+ footer={
435
+ <>
436
+ <Button
437
+ variant="outline"
438
+ color="secondary"
439
+ size="sm"
440
+ onClick={() => setAberto(false)}
441
+ >
442
+ Fechar
443
+ </Button>
444
+ <Button variant="filled" size="sm" iconLeft={<Receipt />}>
445
+ Registrar pagamento
446
+ </Button>
447
+ </>
448
+ }
449
+ >
450
+ {/* Métricas do REGISTRO. Ver o JSDoc do `MetricaCartoes` pro porquê de não ser `Kpi`
451
+ e pra alternativa em faixa dividida que foi testada e descartada aqui. */}
452
+ <FloatingPanelSection title="Métricas">
453
+ <MetricaCartoes itens={METRICAS} />
454
+ </FloatingPanelSection>
455
+
456
+ {/* Metas do registro. Fica junto das métricas porque as duas respondem "como este
457
+ registro está?" — os campos abaixo respondem "quais são os dados dele". */}
458
+ <FloatingPanelSection title="Construção PRO (mês)">
459
+ <ListaDeProgresso situacao={CONSTRUCAO.situacao} itens={CONSTRUCAO.itens} />
460
+ </FloatingPanelSection>
461
+
462
+ <FloatingPanelSection title="Empresa">
463
+ <FloatingPanelField label="Razão Social" value="Estrela do Sul Alimentos S.A." />
464
+ <FloatingPanelField
465
+ label="CNPJ"
466
+ value={<span className="tabular-nums">{REGISTRO.documento}</span>}
467
+ />
468
+ </FloatingPanelSection>
469
+
470
+ {/* Entidade, não campo: banco tem marca, nome e agência/conta. Ver LinhaDeEntidade. */}
471
+ <FloatingPanelSection title="Conta bancária">
472
+ <LinhaDeEntidade
473
+ hex={CONTA.hex}
474
+ iniciais={CONTA.iniciais}
475
+ nome={CONTA.banco}
476
+ detalhe={`Ag ${CONTA.agencia} · ${CONTA.conta}`}
477
+ />
478
+ </FloatingPanelSection>
479
+
480
+ <FloatingPanelSection title="Financeiro">
481
+ <FloatingPanelField
482
+ label="Volume mensal"
483
+ value={<span className="tabular-nums">R$ 72.147,00</span>}
484
+ />
485
+ <FloatingPanelField
486
+ label="Comissão"
487
+ value={<span className="tabular-nums">8,9%</span>}
488
+ />
489
+ <FloatingPanelField label="Saque automático" value="Ativado" />
490
+ {/* Vários valores num campo → Chips, não texto separado por vírgula: cada método é
491
+ um item discreto, e a pílula é o que comunica isso. */}
492
+ <FloatingPanelField
493
+ label="Métodos"
494
+ value={
495
+ <span className="flex flex-wrap items-center justify-end gap-gp-sm">
496
+ {METODOS.map((m) => (
497
+ <Chip key={m} color="neutral" variant="soft" size="sm">
498
+ {m}
499
+ </Chip>
500
+ ))}
501
+ </span>
502
+ }
503
+ />
504
+ </FloatingPanelSection>
505
+
506
+ {/* E-mail e telefone são AÇÕES, não texto: `mailto:`/`tel:` com cor de link.
507
+ `break-all` no e-mail porque endereço longo não tem onde quebrar e estouraria
508
+ a coluna da direita. */}
509
+ <FloatingPanelSection title="Contato">
510
+ <FloatingPanelField
511
+ label="Email"
512
+ value={
513
+ <a
514
+ href={`mailto:${CONTATO.email}`}
515
+ className="break-all text-fg-brand hover:underline"
516
+ >
517
+ {CONTATO.email}
518
+ </a>
519
+ }
520
+ />
521
+ <FloatingPanelField
522
+ label="Telefone"
523
+ value={
524
+ <a
525
+ href={`tel:${CONTATO.telefone.replace(/\D/g, "")}`}
526
+ className="tabular-nums text-fg-brand hover:underline"
527
+ >
528
+ {CONTATO.telefone}
529
+ </a>
530
+ }
531
+ />
532
+ <FloatingPanelField label="Localização" value={CONTATO.local} />
533
+ </FloatingPanelSection>
534
+
535
+ <FloatingPanelSection title="Gestão">
536
+ {/* Pessoa dentro de um campo: avatar `xs` (20px) cabe na linha do valor sem esticar
537
+ a altura — o `lg` da LinhaDeEntidade quebraria o ritmo dos campos. */}
538
+ <FloatingPanelField
539
+ label="Gestor da conta"
540
+ value={
541
+ <span className="flex items-center justify-end gap-gp-sm">
542
+ <Avatar size="xs" colorHex={GESTOR.hex} aria-label={GESTOR.nome}>
543
+ {GESTOR.iniciais}
544
+ </Avatar>
545
+ {GESTOR.nome}
546
+ </span>
547
+ }
548
+ />
549
+ <FloatingPanelField label="Cliente desde" value="12 de março de 2024" />
550
+ <FloatingPanelField label="Última movimentação" value="há 3 dias" />
551
+ </FloatingPanelSection>
552
+ </FloatingPanel>
553
+ </>
554
+ );
555
+ }