@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.
- package/dist-lib/ai/blocos/chart/budget-breakdown.tsx +174 -0
- package/dist-lib/ai/blocos/indice.md +43 -0
- package/dist-lib/ai/blocos/paneldetail/detalhe-com-tabela.tsx +450 -0
- package/dist-lib/ai/blocos/paneldetail/detalhe-de-tarefa-com-abas.tsx +570 -0
- package/dist-lib/ai/blocos/paneldetail/detalhe-do-registro.tsx +555 -0
- package/dist-lib/ai/componentes/AlertModal.md +47 -0
- package/dist-lib/ai/componentes/AppShell.md +117 -0
- package/dist-lib/ai/componentes/Breadcrumb.md +187 -0
- package/dist-lib/ai/componentes/Button.md +116 -0
- package/dist-lib/ai/componentes/ButtonGroup.md +162 -0
- package/dist-lib/ai/componentes/CardCheckbox.md +99 -0
- package/dist-lib/ai/componentes/CardOption.md +133 -0
- package/dist-lib/ai/componentes/Chart.md +93 -0
- package/dist-lib/ai/componentes/Chip.md +68 -0
- package/dist-lib/ai/componentes/ChoroplethMap.md +118 -0
- package/dist-lib/ai/componentes/ColorPicker.md +70 -0
- package/dist-lib/ai/componentes/Combobox.md +51 -0
- package/dist-lib/ai/componentes/ConversationListItem.md +90 -0
- package/dist-lib/ai/componentes/DataList.md +111 -0
- package/dist-lib/ai/componentes/DataTable.md +867 -0
- package/dist-lib/ai/componentes/DatePicker.md +84 -0
- package/dist-lib/ai/componentes/DateSeparatorChip.md +59 -0
- package/dist-lib/ai/componentes/EmptyState.md +72 -0
- package/dist-lib/ai/componentes/FileUploadField.md +95 -0
- package/dist-lib/ai/componentes/FloatingPanel.md +118 -0
- package/dist-lib/ai/componentes/FooterTable.md +62 -0
- package/dist-lib/ai/componentes/FormField.md +110 -0
- package/dist-lib/ai/componentes/Gantt.md +552 -0
- package/dist-lib/ai/componentes/Header.md +98 -0
- package/dist-lib/ai/componentes/Icon.md +65 -0
- package/dist-lib/ai/componentes/Kanban.md +343 -0
- package/dist-lib/ai/componentes/Kpi.md +103 -0
- package/dist-lib/ai/componentes/List.md +61 -0
- package/dist-lib/ai/componentes/MarkdownText.md +59 -0
- package/dist-lib/ai/componentes/MenuSidebar.md +128 -0
- package/dist-lib/ai/componentes/MessageAck.md +53 -0
- package/dist-lib/ai/componentes/MessageBubble.md +115 -0
- package/dist-lib/ai/componentes/MessageComposer.md +80 -0
- package/dist-lib/ai/componentes/MessageVariablesPicker.md +104 -0
- package/dist-lib/ai/componentes/Modal.md +88 -0
- package/dist-lib/ai/componentes/MonthYearPicker.md +49 -0
- package/dist-lib/ai/componentes/PageHeader.md +129 -0
- package/dist-lib/ai/componentes/Panel.md +84 -0
- package/dist-lib/ai/componentes/Scheduler.md +421 -0
- package/dist-lib/ai/componentes/ScreenLoader.md +60 -0
- package/dist-lib/ai/componentes/SingleMenuSidebar.md +171 -0
- package/dist-lib/ai/componentes/Spinner.md +52 -0
- package/dist-lib/ai/componentes/Table.md +192 -0
- package/dist-lib/ai/componentes/TableToolbar.md +87 -0
- package/dist-lib/ai/componentes/TabsNavigation.md +152 -0
- package/dist-lib/ai/componentes/Toast.md +49 -0
- package/dist-lib/ai/componentes/_primitivos.md +74 -0
- package/dist-lib/ai/componentes/avatar-ig.md +181 -0
- package/dist-lib/ai/componentes/indice.json +49 -0
- package/dist-lib/ai/exemplos/app-shell/app-shell-example.tsx +140 -0
- package/dist-lib/ai/exemplos/app-shell/index.ts +3 -0
- package/dist-lib/ai/exemplos/app-shell/nav-data.ts +97 -0
- package/dist-lib/ai/exemplos/app-shell/routes.tsx +75 -0
- package/dist-lib/ai/exemplos/chat/chat-screen.tsx +152 -0
- package/dist-lib/ai/exemplos/chat/chat-v2-mocks.ts +171 -0
- package/dist-lib/ai/exemplos/chat/chat-v2.styles.ts +23 -0
- package/dist-lib/ai/exemplos/chat/chat-v2.types.ts +81 -0
- package/dist-lib/ai/exemplos/chat/components/ChannelDot/channel-dot.tsx +28 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationActionsMenu/conversation-actions-menu.tsx +83 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationActionsMenu/index.ts +4 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.styles.ts +22 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.tsx +203 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.types.ts +10 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationColumn/index.ts +2 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.styles.ts +55 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.tsx +52 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.types.ts +7 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationListItem/index.ts +2 -0
- package/dist-lib/ai/exemplos/chat/components/DateSeparator/date-separator.tsx +18 -0
- package/dist-lib/ai/exemplos/chat/components/DetailField/detail-field.tsx +23 -0
- package/dist-lib/ai/exemplos/chat/components/DetailSection/detail-section.tsx +41 -0
- package/dist-lib/ai/exemplos/chat/components/DetailSection/index.ts +1 -0
- package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.styles.ts +30 -0
- package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.tsx +142 -0
- package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.types.ts +16 -0
- package/dist-lib/ai/exemplos/chat/components/DetailsColumn/index.ts +5 -0
- package/dist-lib/ai/exemplos/chat/components/FilterRow/filter-row.styles.ts +37 -0
- package/dist-lib/ai/exemplos/chat/components/FilterRow/filter-row.tsx +45 -0
- package/dist-lib/ai/exemplos/chat/components/FilterRow/index.ts +1 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.styles.ts +27 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.tsx +66 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.types.ts +13 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-rail.tsx +38 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/index.ts +6 -0
- package/dist-lib/ai/exemplos/chat/components/MessageBubble/index.ts +2 -0
- package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.styles.ts +37 -0
- package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.tsx +29 -0
- package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.types.ts +5 -0
- package/dist-lib/ai/exemplos/chat/components/PersonAvatar/index.ts +5 -0
- package/dist-lib/ai/exemplos/chat/components/PersonAvatar/person-avatar.tsx +31 -0
- package/dist-lib/ai/exemplos/chat/components/QueueColumn/index.ts +2 -0
- package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.styles.ts +19 -0
- package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.tsx +140 -0
- package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.types.ts +13 -0
- package/dist-lib/ai/exemplos/chat/components/RailItem/index.ts +1 -0
- package/dist-lib/ai/exemplos/chat/components/RailItem/rail-item.styles.ts +30 -0
- package/dist-lib/ai/exemplos/chat/components/RailItem/rail-item.tsx +42 -0
- package/dist-lib/ai/exemplos/chat/hooks/use-resizable.ts +98 -0
- package/dist-lib/ai/exemplos/chat/index.ts +1 -0
- package/dist-lib/ai/exemplos/clientes/_table-data.ts +59 -0
- package/dist-lib/ai/exemplos/clientes/clientes-screen.tsx +505 -0
- package/dist-lib/ai/exemplos/clientes/clientes-showcase-mocks.ts +117 -0
- package/dist-lib/ai/exemplos/clientes/clientes-showcase.styles.ts +13 -0
- package/dist-lib/ai/exemplos/clientes/clientes-showcase.types.ts +16 -0
- package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.styles.ts +28 -0
- package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.tsx +303 -0
- package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.types.ts +14 -0
- package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/index.ts +2 -0
- package/dist-lib/ai/exemplos/clientes/components/NovoClienteDrawer/index.ts +2 -0
- package/dist-lib/ai/exemplos/clientes/components/NovoClienteDrawer/novo-cliente-drawer.tsx +207 -0
- package/dist-lib/ai/exemplos/clientes/index.ts +1 -0
- package/dist-lib/ai/exemplos/dashboard/dashboard-brazil-map.ts +33 -0
- package/dist-lib/ai/exemplos/dashboard/dashboard-screen.tsx +1110 -0
- package/dist-lib/ai/exemplos/dashboard/index.ts +1 -0
- package/dist-lib/ai/exemplos/edit-page/components/StepNav.tsx +81 -0
- package/dist-lib/ai/exemplos/edit-page/components/section-card.tsx +85 -0
- package/dist-lib/ai/exemplos/edit-page/edit-page-screen.tsx +234 -0
- package/dist-lib/ai/exemplos/edit-page/index.ts +1 -0
- package/dist-lib/ai/exemplos/finance/_table-data.ts +57 -0
- package/dist-lib/ai/exemplos/finance/clientes-financeiro-mocks.ts +227 -0
- package/dist-lib/ai/exemplos/finance/clientes-financeiro.types.ts +75 -0
- package/dist-lib/ai/exemplos/finance/clientes-showcase-mocks.ts +117 -0
- package/dist-lib/ai/exemplos/finance/clientes-showcase.styles.ts +13 -0
- package/dist-lib/ai/exemplos/finance/clientes-showcase.types.ts +16 -0
- package/dist-lib/ai/exemplos/finance/components/EditarFinanceDrawer/editar-finance-drawer.tsx +241 -0
- package/dist-lib/ai/exemplos/finance/components/EditarFinanceDrawer/index.ts +5 -0
- package/dist-lib/ai/exemplos/finance/components/ExtratoExpansion/extrato-expansion.tsx +172 -0
- package/dist-lib/ai/exemplos/finance/components/ExtratoExpansion/index.ts +1 -0
- package/dist-lib/ai/exemplos/finance/components/FinanceDetailPanel/finance-detail-panel.tsx +241 -0
- package/dist-lib/ai/exemplos/finance/components/FinanceDetailPanel/index.ts +2 -0
- package/dist-lib/ai/exemplos/finance/components/NovoClienteDrawer/index.ts +2 -0
- package/dist-lib/ai/exemplos/finance/components/NovoClienteDrawer/novo-cliente-drawer.tsx +207 -0
- package/dist-lib/ai/exemplos/finance/components/SacarDialog/index.ts +2 -0
- package/dist-lib/ai/exemplos/finance/components/SacarDialog/sacar-dialog.tsx +346 -0
- package/dist-lib/ai/exemplos/finance/finance-screen.tsx +821 -0
- package/dist-lib/ai/exemplos/finance/index.ts +1 -0
- package/dist-lib/ai/exemplos/gantt/_gantt-data.tsx +402 -0
- package/dist-lib/ai/exemplos/gantt/gantt-screen.tsx +512 -0
- package/dist-lib/ai/exemplos/gantt/index.ts +1 -0
- package/dist-lib/ai/exemplos/login/index.ts +1 -0
- package/dist-lib/ai/exemplos/login/login-screen.tsx +246 -0
- package/dist-lib/ai/exemplos/mapa-rede/components/ConsultorDetailPanel/consultor-detail-panel.tsx +158 -0
- package/dist-lib/ai/exemplos/mapa-rede/components/ConsultorDetailPanel/index.ts +2 -0
- package/dist-lib/ai/exemplos/mapa-rede/index.ts +1 -0
- package/dist-lib/ai/exemplos/mapa-rede/mapa-de-rede-mocks.ts +655 -0
- package/dist-lib/ai/exemplos/mapa-rede/mapa-de-rede.types.ts +53 -0
- package/dist-lib/ai/exemplos/mapa-rede/mapa-rede-screen.tsx +227 -0
- package/dist-lib/ai/exemplos/order-detail/components/ActivityTab.tsx +77 -0
- package/dist-lib/ai/exemplos/order-detail/components/AttachmentsTab.tsx +45 -0
- package/dist-lib/ai/exemplos/order-detail/components/CommentsTab.tsx +111 -0
- package/dist-lib/ai/exemplos/order-detail/components/DetailsTab.tsx +189 -0
- package/dist-lib/ai/exemplos/order-detail/components/OverviewTab.tsx +200 -0
- package/dist-lib/ai/exemplos/order-detail/components/section-card.tsx +85 -0
- package/dist-lib/ai/exemplos/order-detail/index.ts +1 -0
- package/dist-lib/ai/exemplos/order-detail/order-detail-screen.tsx +119 -0
- package/dist-lib/ai/exemplos/order-detail/order-mocks.ts +186 -0
- package/dist-lib/ai/exemplos/order-detail/order.types.ts +118 -0
- package/dist-lib/ai/global/componentes.md +217 -0
- package/dist-lib/ai/global/composicao.md +182 -0
- package/dist-lib/ai/indice.json +192 -0
- package/dist-lib/ai/lint/ds-lint-patterns.mjs +115 -0
- package/dist-lib/ai/manifest.json +42 -0
- package/dist-lib/ai/regras/design.md +88 -0
- package/dist-lib/ai/regras/temas.md +192 -0
- package/dist-lib/ai/regras-por-componente.json +103 -0
- package/dist-lib/ai/roteiros/app-builder/roteiro.md +155 -0
- package/dist-lib/ai/roteiros/auth-builder/roteiro.md +42 -0
- package/dist-lib/ai/roteiros/cards/roteiro.md +34 -0
- package/dist-lib/ai/roteiros/charts/roteiro.md +32 -0
- package/dist-lib/ai/roteiros/chat/roteiro.md +31 -0
- package/dist-lib/ai/roteiros/crud-builder/blueprint.md +63 -0
- package/dist-lib/ai/roteiros/crud-builder/entrevista.md +139 -0
- package/dist-lib/ai/roteiros/crud-builder/geracao.md +113 -0
- package/dist-lib/ai/roteiros/crud-builder/roteiro.md +89 -0
- package/dist-lib/ai/roteiros/dashboard-builder/blueprint.md +47 -0
- package/dist-lib/ai/roteiros/dashboard-builder/entrevista.md +62 -0
- package/dist-lib/ai/roteiros/dashboard-builder/geracao.md +88 -0
- package/dist-lib/ai/roteiros/dashboard-builder/roteiro.md +88 -0
- package/dist-lib/ai/roteiros/drawers/roteiro.md +41 -0
- package/dist-lib/ai/roteiros/list-builder/blueprint.md +94 -0
- package/dist-lib/ai/roteiros/list-builder/entrevista.md +188 -0
- package/dist-lib/ai/roteiros/list-builder/geracao.md +63 -0
- package/dist-lib/ai/roteiros/list-builder/roteiro.md +91 -0
- package/dist-lib/ai/roteiros/module-replicator/roteiro.md +56 -0
- package/dist-lib/ai/roteiros/page-detail/roteiro.md +32 -0
- package/dist-lib/ai/roteiros/page-edit/roteiro.md +31 -0
- package/dist-lib/ai/roteiros/screen-composer/roteiro.md +95 -0
- package/package.json +4 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# AlertModal — USAGE
|
|
2
|
+
|
|
3
|
+
Modal de confirmação destrutiva com tone semântico (danger/warning/success/neutral).
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
- Confirmar ação destrutiva (excluir, arquivar, revogar)
|
|
7
|
+
- Pedir confirmação de decisão crítica antes de executar
|
|
8
|
+
|
|
9
|
+
## Import
|
|
10
|
+
```tsx
|
|
11
|
+
import { AlertModal } from "@/components/ui/AlertModal";
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Props essenciais
|
|
15
|
+
| Prop | Tipo | Default | Função |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `open` | boolean | — | Controla visibilidade |
|
|
18
|
+
| `onOpenChange` | (open: boolean) => void | — | Callback de fechamento |
|
|
19
|
+
| `tone` | "default" \| "neutral" \| "danger" \| "warning" \| "success" | "default" | Cor semântica |
|
|
20
|
+
| `title` | ReactNode | — | Título do modal |
|
|
21
|
+
| `description` | ReactNode | — | Texto explicativo |
|
|
22
|
+
| `confirmLabel` | ReactNode | "Confirmar" | Conteúdo do botão primary |
|
|
23
|
+
| `cancelLabel` | ReactNode | "Cancelar" | Conteúdo do botão secondary |
|
|
24
|
+
| `hideCancel` | boolean | false | Esconde o botão Cancel (modal de aviso só com OK) |
|
|
25
|
+
| `hideClose` | boolean | false | Esconde o botão X de fechar no canto superior direito |
|
|
26
|
+
| `onConfirm` | () => void | — | Ação ao confirmar |
|
|
27
|
+
| `loading` | boolean | false | Trava interação durante async — **os 4 caminhos de dismiss**, inclusive ESC |
|
|
28
|
+
| `icon` | ReactNode \| null | — | Ícone customizado; `null` esconde; omitir usa o default do tone |
|
|
29
|
+
|
|
30
|
+
## Exemplo mínimo
|
|
31
|
+
```tsx
|
|
32
|
+
<AlertModal
|
|
33
|
+
open={confirmDelete}
|
|
34
|
+
onOpenChange={setConfirmDelete}
|
|
35
|
+
tone="danger"
|
|
36
|
+
title="Excluir cliente?"
|
|
37
|
+
description="Esta ação não pode ser desfeita"
|
|
38
|
+
onConfirm={handleDelete}
|
|
39
|
+
/>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Cuidados / Gotchas
|
|
43
|
+
- Quando `loading=true`, modal **não fecha automaticamente** — consumer chama `onOpenChange(false)` após async terminar. Vale para **todos** os caminhos: Confirmar, Cancelar, X **e ESC**. O ESC era o que faltava (era o único que não passa por botão): num delete assíncrono ele fechava o modal com a requisição em voo, e o `onOpenChange` do consumidor era chamado. Consertado com `onEscapeKeyDown` + `preventDefault`, então nem o evento chega
|
|
44
|
+
- `tone="danger"` aplica cor critical ao botão de confirm
|
|
45
|
+
- `icon={null}` esconde o ícone; **omitir** a prop usa o ícone default do tone (`tone="default"` não tem ícone)
|
|
46
|
+
- Pra forçar decisão explícita (sem escape pelo X), use `hideClose`; pra modal informativo só com OK, use `hideCancel`
|
|
47
|
+
- Pra "alert" não-destrutivo, prefira `<Modal>` simples
|