@snksergio/design-system 0.60.0 → 0.62.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 (65) hide show
  1. package/dist-lib/ai/componentes/AlertModal.md +47 -0
  2. package/dist-lib/ai/componentes/AppShell.md +117 -0
  3. package/dist-lib/ai/componentes/Breadcrumb.md +187 -0
  4. package/dist-lib/ai/componentes/Button.md +116 -0
  5. package/dist-lib/ai/componentes/ButtonGroup.md +162 -0
  6. package/dist-lib/ai/componentes/CardCheckbox.md +99 -0
  7. package/dist-lib/ai/componentes/CardOption.md +133 -0
  8. package/dist-lib/ai/componentes/Chart.md +93 -0
  9. package/dist-lib/ai/componentes/Chip.md +68 -0
  10. package/dist-lib/ai/componentes/ChoroplethMap.md +118 -0
  11. package/dist-lib/ai/componentes/ColorPicker.md +70 -0
  12. package/dist-lib/ai/componentes/Combobox.md +51 -0
  13. package/dist-lib/ai/componentes/ConversationListItem.md +90 -0
  14. package/dist-lib/ai/componentes/DataList.md +111 -0
  15. package/dist-lib/ai/componentes/DataTable.md +867 -0
  16. package/dist-lib/ai/componentes/DatePicker.md +84 -0
  17. package/dist-lib/ai/componentes/DateSeparatorChip.md +59 -0
  18. package/dist-lib/ai/componentes/EmptyState.md +72 -0
  19. package/dist-lib/ai/componentes/FileUploadField.md +95 -0
  20. package/dist-lib/ai/componentes/FloatingPanel.md +118 -0
  21. package/dist-lib/ai/componentes/FooterTable.md +62 -0
  22. package/dist-lib/ai/componentes/FormField.md +110 -0
  23. package/dist-lib/ai/componentes/Gantt.md +552 -0
  24. package/dist-lib/ai/componentes/Header.md +98 -0
  25. package/dist-lib/ai/componentes/Icon.md +65 -0
  26. package/dist-lib/ai/componentes/Kanban.md +343 -0
  27. package/dist-lib/ai/componentes/Kpi.md +103 -0
  28. package/dist-lib/ai/componentes/List.md +61 -0
  29. package/dist-lib/ai/componentes/MarkdownText.md +59 -0
  30. package/dist-lib/ai/componentes/MenuSidebar.md +128 -0
  31. package/dist-lib/ai/componentes/MessageAck.md +53 -0
  32. package/dist-lib/ai/componentes/MessageBubble.md +115 -0
  33. package/dist-lib/ai/componentes/MessageComposer.md +80 -0
  34. package/dist-lib/ai/componentes/MessageVariablesPicker.md +104 -0
  35. package/dist-lib/ai/componentes/Modal.md +88 -0
  36. package/dist-lib/ai/componentes/MonthYearPicker.md +49 -0
  37. package/dist-lib/ai/componentes/PageHeader.md +129 -0
  38. package/dist-lib/ai/componentes/Panel.md +84 -0
  39. package/dist-lib/ai/componentes/Scheduler.md +421 -0
  40. package/dist-lib/ai/componentes/ScreenLoader.md +60 -0
  41. package/dist-lib/ai/componentes/SingleMenuSidebar.md +171 -0
  42. package/dist-lib/ai/componentes/Spinner.md +52 -0
  43. package/dist-lib/ai/componentes/Table.md +192 -0
  44. package/dist-lib/ai/componentes/TableToolbar.md +87 -0
  45. package/dist-lib/ai/componentes/TabsNavigation.md +152 -0
  46. package/dist-lib/ai/componentes/Toast.md +49 -0
  47. package/dist-lib/ai/componentes/_primitivos.md +74 -0
  48. package/dist-lib/ai/componentes/avatar-ig.md +181 -0
  49. package/dist-lib/ai/componentes/indice.json +49 -0
  50. package/dist-lib/ai/exemplos/dashboard/dashboard-brazil-map.ts +33 -0
  51. package/dist-lib/ai/exemplos/dashboard/dashboard-screen.tsx +1110 -0
  52. package/dist-lib/ai/exemplos/dashboard/index.ts +1 -0
  53. package/dist-lib/ai/global/componentes.md +217 -0
  54. package/dist-lib/ai/global/composicao.md +182 -0
  55. package/dist-lib/ai/indice.json +177 -0
  56. package/dist-lib/ai/lint/ds-lint-patterns.mjs +115 -0
  57. package/dist-lib/ai/manifest.json +16 -0
  58. package/dist-lib/ai/regras/design.md +88 -0
  59. package/dist-lib/ai/regras/temas.md +192 -0
  60. package/dist-lib/ai/regras-por-componente.json +103 -0
  61. package/dist-lib/ai/roteiros/dashboard/blueprint.md +47 -0
  62. package/dist-lib/ai/roteiros/dashboard/entrevista.md +62 -0
  63. package/dist-lib/ai/roteiros/dashboard/geracao.md +88 -0
  64. package/dist-lib/ai/roteiros/dashboard/roteiro.md +88 -0
  65. package/package.json +4 -1
@@ -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
@@ -0,0 +1,117 @@
1
+ # AppShell — USAGE
2
+
3
+ <!-- ds:regras
4
+ - tem áreas separadas (Comercial, Financeiro…)? → `sidebar="menu"` + `contexts`. Não tem? → `"single"` + `categories`, sem `sidebarModules` nem `sidebarShowSearch`
5
+ - NÃO passe `sidebarLogo`: o default é a marca iGreen. Só com marca própria pedida explicitamente
6
+ - `sidebarTitle` = nome do projeto (vai à direita da logo) — pergunte, não invente
7
+ -->
8
+
9
+ Template de aplicação completo: MenuSidebar (rail + panel) + Header sticky + body com slot livre.
10
+
11
+ ## Quando usar
12
+ - Páginas full-app (Showcases, CRUD, Chat, Dashboard)
13
+ - Quando precisar de contexts (workspace switcher) + breadcrumb + user menu unificados
14
+
15
+ ## Import
16
+ ```tsx
17
+ import { AppShell } from "@/components/ui/AppShell";
18
+ ```
19
+
20
+ ## Qual sidebar — `menu` (default) × `single`
21
+
22
+ O shell monta **uma das duas** sidebars. O tipo é **união discriminada**: cada escolha exige
23
+ o seu próprio conjunto de dados, e o TS cobra no editor.
24
+
25
+ | `sidebar` | quando | exige |
26
+ |---|---|---|
27
+ | `"menu"` (default) | app com **áreas distintas** (Comercial, Financeiro…), cada uma com menu próprio | `contexts` |
28
+ | `"single"` | **sistema único**, um menu só — busca opcional | `categories` + `sidebarLogo` + `sidebarTitle` |
29
+
30
+ ```tsx
31
+ <AppShell
32
+ sidebar="single"
33
+ categories={CATEGORIES}
34
+ sidebarLogo={<MinhaLogo />}
35
+ sidebarTitle="Meu Sistema"
36
+ sidebarShowSearch
37
+ activeItemId={ativo}
38
+ onSidebarItemClick={setAtivo}
39
+ breadcrumb={[{ label: "Sistema" }]}
40
+ >…</AppShell>
41
+ ```
42
+
43
+ **O toggle do Header funciona nas duas sem você cabear nada.** O mapeamento interno difere
44
+ porque os componentes modelam o estado de formas diferentes — `MenuSidebar` tem
45
+ `panelCollapsed` + drawer no mobile; a single tem `expanded`, e no mobile o `expanded` **é** a
46
+ visibilidade (expandida ocupa 100% da largura, recolhida some).
47
+
48
+ ⚠️ **`onSidebarItemClick` é separado do `onItemClick`**, e não é redundância: o `MenuSidebar`
49
+ entrega o **item** (`SidebarMenuItem`), a single entrega o **`id`**. Mesmo nome faria você
50
+ receber um tipo e escrever pro outro.
51
+
52
+ ⚠️ **Por que união e não props opcionais:** deixar `contexts` opcional trocaria erro de
53
+ compilação por falha silenciosa — ausente com a sidebar de menu, o rail renderiza **vazio**,
54
+ sem erro nenhum.
55
+
56
+ ## Props essenciais
57
+ | Prop | Tipo | Default | Função |
58
+ |---|---|---|---|
59
+ | `sidebar` | `"menu" \| "single"` | `"menu"` | Qual menu lateral montar — ver a seção acima |
60
+ | `fillHeight` | boolean | `false` | O shell obedece a altura do **pai** (`h-full`) em vez de 100vh. **Ligue quando embutir o shell em algo com altura** (layout com footer, aba, preview): sem isso ele transborda e o `overflow-hidden` do container corta o rodapé do body junto com o padding — o sintoma é "conteúdo colado na borda", e não é falta de padding. ⚠️ exige pai com altura |
61
+ | `contexts` | SidebarContext[] | — | Lista de workspaces no rail (**só** com `sidebar="menu"`) |
62
+ | `categories` | SingleMenuCategory[] | — | Categorias do menu (**só** com `sidebar="single"`) |
63
+ | `sidebarLogo` | ReactNode | **marca iGreen** | Logo do header da sidebar single. **Omita** pra ficar com a marca — só passe se o app tem marca própria |
64
+ | `sidebarTitle` | string | — | **Nome do projeto**, à direita da logo. Obrigatória de propósito: é o que o DS não adivinha |
65
+ | `activeItemId` | string | — | Item ativo da single (a variante `menu` usa `activeItemHref`) |
66
+ | `onSidebarItemClick` | (id: string) => void | — | Clique em item da single |
67
+ | `sidebarModules` | SingleMenuModuleConfig[] | — | Módulos com menu próprio — o seletor troca o conjunto de categorias |
68
+ | `sidebarShowSearch` | boolean | — | Busca no topo da sidebar (é um botão que abre command palette, não um input) |
69
+ | `sidebarSearchPlaceholder` | string | — | Placeholder da busca **da sidebar** — distinto do `searchPlaceholder`, que é do Header |
70
+ | `defaultActiveContextId` | string | primeiro do array | Workspace inicial (uncontrolled) |
71
+ | `activeContextId` | string | — | Workspace ativo (controlled) |
72
+ | `onContextChange` | (id: string) => void | — | Callback de troca de workspace |
73
+ | `defaultActiveItemHref` | string | — | Item do panel ativo inicial (uncontrolled) |
74
+ | `activeItemHref` | string | — | Item do panel ativo (controlled) |
75
+ | `onItemClick` | (item, event?) => void | — | Clique em item do panel. **2º arg é o `MouseEvent`** |
76
+ | `renderLink` | (props) => ReactNode | — | ⭐ **Integração com router** — troca o `<a>` interno pelo `<Link>`. Ver `MenuSidebar/USAGE.md` §Integração com router |
77
+ | `brandHref` | string | `"/"` | Destino do brand no rail; `""` torna não-navegável |
78
+ | `onBrandClick` | (e) => void | — | Clique no brand |
79
+ | `breadcrumb` | HeaderBreadcrumbItem[] | — | Caminho atual exibido no Header |
80
+ | `commandGroups` | HeaderCommandGroup[] | — | Command palette (⌘K) |
81
+ | `notifications` | { items, onMarkAllRead, onViewAll } | — | Dropdown de notificações |
82
+ | `messages` | { items, onNewMessage, onExpand, onViewAll } | — | Dropdown de mensagens |
83
+ | `theme` | string | — | Tema atual (light/dark) |
84
+ | `onThemeChange` | (id: string) => void | — | Callback de troca de tema |
85
+ | `themeOptions` | HeaderThemeOption[] | — | Opções de tema disponíveis |
86
+ | `headerRightSlot` | ReactNode | — | Slot extra no canto direito do Header |
87
+ | `user` | AppShellUser | — | Avatar + user menu no rail bottom |
88
+ | `layout` | string ("fluid" \| "compact") | comportamento "fluid" | Densidade do body (qualquer valor ≠ "compact" cai em fluid) |
89
+ | `onLayoutChange` | (id: string) => void | — | Callback do switcher Fluido/Compacto do user menu |
90
+ | `layoutOptions` | AppShellLayoutOption[] | — | Opções do switcher de layout |
91
+ | `onSettings` | () => void | — | Ação "Configurações" do user menu (item escondido se omitido) |
92
+ | `onLogout` | () => void | — | Ação "Sair" do user menu (item escondido se omitido) |
93
+ | `menuCollapsed` | boolean | — | Sidebar colapsado (controlled) |
94
+ | `defaultMenuCollapsed` | boolean | **responsivo** | Estado inicial do collapse (uncontrolled). Omitido: colapsado `<1536px`, expandido acima. Valor explícito vence — inclusive `false`. Só no mount, resize não re-colapsa |
95
+ | `onMenuCollapseChange` | (collapsed: boolean) => void | — | Callback no toggle do collapse (persistir entre sessões) |
96
+
97
+ ## Exemplo mínimo
98
+ ```tsx
99
+ <AppShell
100
+ contexts={APP_SHELL_CONTEXTS}
101
+ defaultActiveContextId="inbox"
102
+ breadcrumb={[{ label: "Clientes" }]}
103
+ theme={theme}
104
+ onThemeChange={setTheme}
105
+ >
106
+ <YourPageContent />
107
+ </AppShell>
108
+ ```
109
+
110
+ ## Cuidados / Gotchas
111
+ - Body interno tem `gap-gp-4xl` (24px) fixo e padding **responsivo em 3 patamares**: 18px `<768`, **24px `768–1535` (notebook)**, 32px `≥1536`. Customize spacing dentro do `children`, não aqui
112
+ - `contexts` mínimo 1; sem isso o rail fica vazio
113
+ - Mobile: `mobileEdgeToEdge` remove padding do body
114
+ - User menu (layout + tema + settings + logout) só renderiza quando `user` é passado; sem ele o rail mantém o avatar default
115
+ - `layout` é controlled-only: sem `onLayoutChange` o switcher Fluido/Compacto do user menu não tem efeito — guarde o valor em state e devolva via `layout`
116
+ - Pra navegação real, use `activeItemHref` + `onItemClick` (controlled) ligados ao router — os `default*` servem só pro modo uncontrolled/preview
117
+ - ⚠️ **Com react-router (ou qualquer router de history), passe `renderLink`**: `renderLink={(p) => <Link {...p} to={p.href} />}`. Sem isso, `href` de path fazia o browser recarregar a página inteira a cada clique de menu — bug real reportado em 2026-08-08, corrigido na v0.38.0. Detalhe e as 5 exceções em `MenuSidebar/USAGE.md` §Integração com router
@@ -0,0 +1,187 @@
1
+ # Breadcrumb
2
+
3
+ <!-- ds:regras
4
+ - o caminho e o seletor vêm do MESMO item: `import { Breadcrumb, BreadcrumbItem, BreadcrumbSwitcher } from "@/components/ui/Breadcrumb"`
5
+ - página de DETALHE (ficha de cliente, UC, contrato, chamado) → o item do registro no breadcrumb é `<BreadcrumbSwitcher>`, não texto: quem está numa ficha quer pular pra outra, não voltar à lista
6
+ - é controlado e **não navega**: `onValueChange` devolve o `value` e quem decide rota/fetch é você
7
+ - no `Header`, o item vira seletor com os TRÊS juntos — `switcher` + `value` + `onValueChange`; faltando um, fica texto
8
+ - escolher valor de FORMULÁRIO não é isto: é `combobox` (o trigger dele tem cara de campo de propósito)
9
+ -->
10
+
11
+ Caminho de navegação — e a variação em que **o item troca o registro aberto**.
12
+
13
+ Os primitivos (`Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`,
14
+ `BreadcrumbPage`, `BreadcrumbSeparator`, `BreadcrumbEllipsis`) e o `BreadcrumbSwitcher` saem
15
+ do mesmo lugar: são a mesma peça, com e sem troca.
16
+
17
+ ## BreadcrumbSwitcher
18
+
19
+ O item do caminho que **troca o registro aberto**: o nome do que está aberto vira gatilho e
20
+ abre uma lista com busca. É o seletor de repositório do GitHub aplicado a cliente, UC,
21
+ contrato — qualquer coisa de que existam muitos.
22
+
23
+ ## Quando usar
24
+
25
+ | Situação | Componente |
26
+ |---|---|
27
+ | Página de detalhe de UM registro entre muitos parecidos | **`BreadcrumbSwitcher`** |
28
+ | Navegar entre seções fixas do caminho | `BreadcrumbLink` (o breadcrumb normal) |
29
+ | Escolher um valor num formulário | `combobox` |
30
+ | Trocar entre sessões abertas ao mesmo tempo | `tabs-navigation` |
31
+
32
+ ## Os dois modos
33
+
34
+ ```tsx
35
+ // 1) pronto — dados entram, cadeia sai (95% das telas)
36
+ <Breadcrumb items={[
37
+ { label: "Clientes", href: "/clientes" },
38
+ { label: cliente.nome }, // sem href = página atual
39
+ ]} />
40
+
41
+ // 2) composição — pra interpor algo ou estilizar item a item
42
+ <Breadcrumb>
43
+ <BreadcrumbList>
44
+ <BreadcrumbItem><BreadcrumbLink href="/clientes">Clientes</BreadcrumbLink></BreadcrumbItem>
45
+ <BreadcrumbSeparator />
46
+ <BreadcrumbItem><Chip size="sm">homologação</Chip></BreadcrumbItem>
47
+ </BreadcrumbList>
48
+ </Breadcrumb>
49
+ ```
50
+
51
+ | Prop da raiz | | |
52
+ |---|---|---|
53
+ | `items` | `BreadcrumbItemData[]` | sem ele, renderiza `children` no primitivo |
54
+ | `size` | `sm` | `md` | `sm` = 13px na cadeia e 16px em item único (é o do `Header`); `md` = 14px (o do primitivo) |
55
+ | `separator` | `ReactNode` | default `ChevronRight` de 14px |
56
+
57
+ ## Import
58
+
59
+ ```tsx
60
+ import { Breadcrumb, BreadcrumbItem, BreadcrumbSwitcher } from "@/components/ui/Breadcrumb";
61
+ ```
62
+
63
+ ## Exemplo mínimo
64
+
65
+ ```tsx
66
+ <BreadcrumbItem>
67
+ <BreadcrumbSwitcher
68
+ value={clienteId}
69
+ onValueChange={abrirCliente}
70
+ options={clientes} // { value, label, leading?, description?, keywords?, group? }
71
+ title="Trocar cliente"
72
+ searchPlaceholder="Buscar por nome ou documento…"
73
+ aria-label="Trocar cliente"
74
+ />
75
+ </BreadcrumbItem>
76
+ ```
77
+
78
+ ## No `Header`
79
+
80
+ O item do breadcrumb do `Header` vira seletor sozinho:
81
+
82
+ ```tsx
83
+ <Header
84
+ breadcrumb={[
85
+ { label: "Clientes", href: "/clientes" },
86
+ {
87
+ label: cliente.nome,
88
+ switcher: clientes,
89
+ value: cliente.id,
90
+ onValueChange: abrirCliente,
91
+ switcherTitle: "Trocar cliente",
92
+ },
93
+ ]}
94
+ />
95
+ ```
96
+
97
+ Precisa dos **três** (`switcher` + `value` + `onValueChange`). Faltando um, o item renderiza
98
+ como texto normal. No celular, onde a cadeia colapsa e sobra só o último item, o seletor
99
+ continua ali.
100
+
101
+ ## Props
102
+
103
+ | Prop | Tipo | Default |
104
+ |---|---|---|
105
+ | `value` / `onValueChange` | `string` / `(v: string) => void` | — (controlado) |
106
+ | `options` | `BreadcrumbSwitcherOption[]` | — |
107
+ | `placeholder` | `ReactNode` — quando o `value` não está na lista | o próprio `value` |
108
+ | `title` | `ReactNode` — cabeçalho do dropdown | — |
109
+ | `searchPlaceholder` | `string` | `"Buscar…"` |
110
+ | `emptyMessage` | `ReactNode` | `"Nada encontrado."` |
111
+ | `footer` | `ReactNode` — fora da área que rola | — |
112
+ | `open` / `onOpenChange` | abertura controlada | — |
113
+ | `align` | `start \| center \| end` | `"start"` |
114
+ | `aria-label` | `string` | `"Trocar registro aberto"` |
115
+
116
+ `BreadcrumbSwitcherOption`: `{ value, label, leading?, description?, keywords?, group? }`.
117
+
118
+ ## `trailing` — conteúdo livre depois do rótulo
119
+
120
+ Qualquer item do modo `items={...}` (e do `breadcrumb` do `Header`) aceita
121
+ `trailing?: ReactNode` — o que vier ali entra no mesmo `<li>`, depois do rótulo
122
+ ou do gatilho:
123
+
124
+ ```tsx
125
+ <Breadcrumb
126
+ items={[
127
+ { label: "Clientes", href: "/clientes" },
128
+ {
129
+ label: cliente.nome,
130
+ switcher: CLIENTES, value: id, onValueChange: setId,
131
+ trailing: (
132
+ <Chip size="sm" variant="soft" color={cliente.statusCor}>
133
+ {cliente.status}
134
+ </Chip>
135
+ ),
136
+ },
137
+ ]}
138
+ />
139
+ ```
140
+
141
+ O slot **não sabe** que o caso de origem foi status: aceita qualquer nó e qualquer
142
+ montagem. E vale pros quatro tipos de item — seletor, link, página atual e texto
143
+ inerte —, não só pro seletor.
144
+
145
+ ⚠️ **Ele é IRMÃO do gatilho, não filho.** É o que torna "qualquer componente"
146
+ verdade: `<button>` dentro de `<button>` é HTML inválido e quebra clique e foco,
147
+ então chip clicável, link ou botão só funcionam porque o slot é externo. A
148
+ consequência é de desenho, não acidente — **clicar no `trailing` não abre a
149
+ lista**, e é o certo: status não é a affordance de "trocar registro".
150
+
151
+ No **modo composição** você não precisa dele: `BreadcrumbItem` é
152
+ `inline-flex items-center gap-gp-sm`, então basta escrever o nó ao lado.
153
+
154
+ ```tsx
155
+ <BreadcrumbItem>
156
+ <BreadcrumbSwitcher … />
157
+ <Chip size="sm" variant="soft" color="success">Ativo</Chip>
158
+ </BreadcrumbItem>
159
+ ```
160
+
161
+ ## Gotchas / cuidados
162
+
163
+ - **Ele não navega.** `onValueChange` devolve o valor; rota, fetch e estado são seus. É o que
164
+ faz o mesmo componente servir pro app com router e pro painel que só troca estado local.
165
+ - **`keywords` é pra o que o usuário sabe de cor e a tela não mostra** — CPF/CNPJ, código
166
+ interno, apelido. A busca filtra por `label` + `keywords`, e o `value` já entra
167
+ automaticamente.
168
+ - **A busca é local E fuzzy.** O `Command` filtra por subsequência com score, não por prefixo:
169
+ procurar um CPF pode trazer junto um CNPJ que compartilha dígitos, com score menor. O certo
170
+ vem em primeiro; não prometa "resultado único" na sua tela. E acima de ~1.000 registros,
171
+ pagine ou busque no servidor antes de montar `options`.
172
+ - **A opção do registro aberto sai com `data-atual`** no DOM — use isso pra estilizar ou
173
+ testar. O `data-selected` do cmdk é outra coisa (o item ativo do teclado), e "tem svg" não
174
+ serve de sinal porque `leading` também é svg.
175
+ - **Grupos saem na ordem de `options`**, não alfabética — “Recentes” antes de “Todos” é
176
+ informação, não acaso.
177
+ - **Passe `aria-label` com o nome do domínio** (“Trocar cliente”). O default genérico funciona,
178
+ mas quem ouve a tela merece o termo certo.
179
+ - **Use `placeholder` quando a lista chega assíncrona**: sem ele, enquanto as opções não
180
+ carregam, o caminho mostra o `value` cru (um id), que lê como bug.
181
+ - **Item único é TÍTULO, não caminho.** Com um item só e `size="sm"`, ele sobe pra 16px/600 —
182
+ uma cadeia de um elemento é o nome da tela. É o comportamento que o `Header` sempre teve.
183
+ - **O `href` do ÚLTIMO item é ignorado**: página atual não navega pra si. E o atual é um
184
+ `BreadcrumbPage`, que o shadcn expõe como `role="link" aria-disabled aria-current="page"` —
185
+ se você testar "não é link" por `getByRole("link")`, vai achar que quebrou.
186
+ - **Não empilhe com um link no mesmo item.** Se o item tem `switcher`, o clique abre a lista —
187
+ um `href` ali seria uma segunda intenção que nunca dispara.
@@ -0,0 +1,116 @@
1
+ # Button
2
+
3
+ Componente interativo para ações. Suporta 5 cores, 4 variantes e 10 tamanhos (5 com label + 5 icon-only).
4
+
5
+ ## Variantes
6
+
7
+ | color + variant | Quando usar |
8
+ |--------------------|-------------|
9
+ | primary + filled | Ação principal da tela, CTA — único por contexto |
10
+ | primary + outline | Ação secundária com destaque de marca |
11
+ | primary + soft | Ação com peso visual reduzido, fundo tonal |
12
+ | primary + ghost | Ação terciária, sem peso visual |
13
+ | secondary + filled | Ação de destaque sem conotação de marca (settings, filtros) |
14
+ | secondary + outline| Ação neutra com borda — uso geral |
15
+ | secondary + soft | Ação neutra com fundo sutil |
16
+ | secondary + ghost | Ação neutra sem peso visual |
17
+ | critical + filled | Ação destrutiva ou irreversível (deletar, remover) |
18
+ | critical + outline | Ação destrutiva com menos peso |
19
+ | critical + soft | Ação destrutiva em contexto de lista |
20
+ | critical + ghost | Ação destrutiva terciária |
21
+ | success + filled | Confirmação positiva, ação concluída com sucesso |
22
+ | success + outline | Feedback positivo com menos peso |
23
+ | success + soft | Feedback positivo em contexto de lista |
24
+ | success + ghost | Feedback positivo terciário |
25
+ | warning + filled | Ação que requer atenção ou cautela |
26
+ | warning + outline | Alerta com menos peso visual |
27
+ | warning + soft | Alerta em contexto de lista |
28
+ | warning + ghost | Alerta terciário |
29
+
30
+ ## Tamanhos
31
+
32
+ | size | Height | Uso |
33
+ |------|--------|-----|
34
+ | `2xs` | 28px (`formHeight.xs`) | Toolbars compactas, inline actions |
35
+ | `xs` | 32px (`formHeight.sm`) | Formulários densos, filtros |
36
+ | `sm` | 36px (`formHeight.md`) | Padrão desktop |
37
+ | `md` | 40px (`formHeight.lg`) | Padrão mobile / CTA — touch-friendly |
38
+ | `lg` | 44px (`formHeight.xl`) | WCAG touch target (44px) |
39
+ | `icon-2xs` | 28×28px (quadrado) | Icon button — toolbars compactas |
40
+ | `icon-xs` | 32×32px (quadrado) | Icon button — formulários densos |
41
+ | `icon-sm` | 36×36px (quadrado) | Icon button — padrão desktop |
42
+ | `icon-md` | 40×40px (quadrado) | Icon button — padrão mobile |
43
+ | `icon-lg` | 44×44px (quadrado) | Icon button — WCAG touch target |
44
+
45
+ Icon sizes são quadrados (width = height) com `p-0` — usar só com ícone, sem label.
46
+ Para WCAG touch target (44px), usar `size="lg"` (`min-h-form-xl`).
47
+
48
+ ## Props
49
+
50
+ | Prop | Tipo | Default | Descrição |
51
+ |------|------|---------|-----------|
52
+ | `color` | `"primary" \| "secondary" \| "critical" \| "success" \| "warning"` | `"primary"` | Cor/intenção semântica |
53
+ | `variant` | `"filled" \| "outline" \| "soft" \| "ghost"` | `"filled"` | Estilo visual |
54
+ | `size` | `"2xs" \| "xs" \| "sm" \| "md" \| "lg" \| "icon-2xs" \| "icon-xs" \| "icon-sm" \| "icon-md" \| "icon-lg"` | `"md"` | Tamanho (prefixo `icon-` = quadrado, icon-only) |
55
+ | `shape` | `"rounded" \| "pill"` | `"rounded"` | Forma — `pill` força `rounded-full` (override do radius da size) |
56
+ | `fullWidth` | `boolean` | `false` | Ocupa 100% da largura do container |
57
+ | `disabled` | `boolean` | `false` | Estado desabilitado |
58
+ | `loading` | `boolean` | `false` | Mostra spinner e desabilita |
59
+ | `iconLeft` | `ReactNode` | — | Ícone antes do texto |
60
+ | `iconRight` | `ReactNode` | — | Ícone após o texto |
61
+ | `className` | `string` | — | Override de classes (tw-merge resolve conflitos) |
62
+ | `type` | `"button" \| "submit" \| "reset"` | `"button"` | Tipo HTML — default "button" para evitar submit acidental |
63
+
64
+ ## Fazer / Não fazer
65
+
66
+ - Usar `color="critical"` para ações destrutivas (deletar, remover)
67
+ - Usar `variant="ghost"` para ações de baixa prioridade
68
+ - Usar `fullWidth` para CTAs em mobile ou modais
69
+ - Máximo 1 botão `filled` por grupo de ações
70
+ - Padrão de grupo: 1 filled + 1 ghost (ou outline)
71
+ - Não usar `filled` para cancelar — usar `ghost`
72
+ - Não usar `disabled` para esconder funcionalidade — apenas quando ação não está disponível
73
+ - Não misturar `primary` e `critical` filled no mesmo grupo
74
+
75
+ ## Exemplo de uso
76
+
77
+ ```tsx
78
+ import { Button } from "@/components/ui/Button";
79
+
80
+ // Ação principal
81
+ <Button color="primary" variant="filled">
82
+ Salvar
83
+ </Button>
84
+
85
+ // Ação secundária
86
+ <Button color="primary" variant="ghost">
87
+ Cancelar
88
+ </Button>
89
+
90
+ // Ação destrutiva
91
+ <Button color="critical" variant="filled" iconLeft={<TrashIcon />}>
92
+ Excluir
93
+ </Button>
94
+
95
+ // Ação neutra com loading
96
+ <Button color="secondary" variant="outline" loading>
97
+ Processando...
98
+ </Button>
99
+
100
+ // Grupo de ações (padrão)
101
+ <div className="flex gap-gp-md">
102
+ <Button color="primary" variant="ghost">Cancelar</Button>
103
+ <Button color="primary" variant="filled">Confirmar</Button>
104
+ </div>
105
+
106
+ // Botão full-width
107
+ <Button color="primary" variant="filled" fullWidth>
108
+ Botão full-width
109
+ </Button>
110
+ ```
111
+
112
+ ## Fonte de verdade
113
+
114
+ - Estilos: `button.styles.ts` (tv())
115
+ - Lógica: `button.tsx`
116
+ - Tipos: `button.types.ts`
@@ -0,0 +1,162 @@
1
+ # ButtonGroup — Guia de uso
2
+
3
+ Componente composto **split button** — um botão principal + um chevron lateral compacto pra ações secundárias. Pattern típico: ação default + dropdown de variantes.
4
+
5
+ > Não confundir com **toolbar/segmented control**. ButtonGroup hoje cobre **split button (2 slots)**. Pra agrupar 3+ botões em "linked toolbar" (Day/Week/Month), criar componente próprio futuro.
6
+
7
+ ---
8
+
9
+ ## Imports
10
+
11
+ ```tsx
12
+ import { ButtonGroup } from "@/components/ui/ButtonGroup";
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Quick start
18
+
19
+ ```tsx
20
+ <ButtonGroup color="primary" variant="filled" size="md">
21
+ <ButtonGroup.Primary onClick={handlePrimary} iconLeft={<Save />}>
22
+ Salvar
23
+ </ButtonGroup.Primary>
24
+ <ButtonGroup.Chevron
25
+ onClick={openDropdown}
26
+ aria-label="Mais opções de salvamento"
27
+ />
28
+ </ButtonGroup>
29
+ ```
30
+
31
+ Visual:
32
+
33
+ ```
34
+ ┌────────────┬──┐
35
+ │ 💾 Salvar │ ⌄│
36
+ └────────────┴──┘
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Props
42
+
43
+ ### `<ButtonGroup>` (wrapper)
44
+
45
+ | Prop | Tipo | Default | Descrição |
46
+ |------|------|---------|-----------|
47
+ | `color` | `"primary" \| "secondary" \| "critical" \| "success" \| "warning"` | `"primary"` | Cor herdada do `<Button>`. Propaga aos slots via context. |
48
+ | `variant` | `"filled" \| "outline" \| "soft" \| "ghost"` | `"filled"` | Estilo visual. Propaga aos slots. |
49
+ | `size` | `"2xs" \| "xs" \| "sm" \| "md" \| "lg"` | `"md"` | Altura. Propaga aos slots. **Icon sizes (`icon-*`) não suportados** — o Chevron já é icon-only quadrado, com dimensão derivada da size do group. |
50
+ | `disabled` | `boolean` | `false` | Desabilita os 2 slots simultaneamente. Override individual permitido. |
51
+ | `children` | `ReactNode` | — | `<ButtonGroup.Primary>` + `<ButtonGroup.Chevron>` |
52
+ | `className` | `string` | — | Override do wrapper externo. |
53
+
54
+ ### `<ButtonGroup.Primary>` (slot principal)
55
+
56
+ Aceita **todas as props do `<Button>`** exceto `shape` e `fullWidth`. Color/variant/size vêm do group por context — pode dar override passando explicitamente.
57
+
58
+ | Prop | Tipo | Default | Descrição |
59
+ |------|------|---------|-----------|
60
+ | `onClick` | `(e) => void` | — | Handler da ação principal. |
61
+ | `iconLeft` / `iconRight` | `ReactNode` | — | Ícone inline. |
62
+ | `loading` | `boolean` | `false` | Mostra spinner + desabilita. |
63
+ | `disabled` | `boolean` | herda do group | Override individual. |
64
+ | `color`/`variant`/`size` | — | herda do group | Override individual permitido. |
65
+
66
+ ### `<ButtonGroup.Chevron>` (slot secundário)
67
+
68
+ Icon button **quadrado** (width = height) — espelha a size do Primary, como um "split" da mesma proporção: `2xs`→28×28, `xs`→32×32, `sm`→36×36, `md`→40×40 (`size-form-lg`), `lg`→44×44. Mesma dimensão de um icon button `icon-*` equivalente do Button. Renderiza `<ChevronDown />` por default.
69
+
70
+ | Prop | Tipo | Default | Descrição |
71
+ |------|------|---------|-----------|
72
+ | `onClick` | `(e) => void` | — | Handler do toggle (geralmente abre dropdown). |
73
+ | `aria-label` | `string` | **obrigatório** | Descrição pro leitor de tela (icon-only). |
74
+ | `icon` | `ReactNode` | `<ChevronDown />` | Customize se precisar (ex: `<MoreVertical />` pra kebab). |
75
+ | `disabled` | `boolean` | herda do group | Override individual. |
76
+ | `color`/`variant`/`size` | — | herda do group | Override permitido. |
77
+
78
+ ---
79
+
80
+ ## Exemplos
81
+
82
+ ### 1. Split button com dropdown
83
+
84
+ ```tsx
85
+ import { useState } from "react";
86
+ import { ButtonGroup } from "@/components/ui/ButtonGroup";
87
+ import { Popover, PopoverTrigger, PopoverContent } from "@/components/shadcn/popover";
88
+
89
+ function SaveSplit() {
90
+ const [open, setOpen] = useState(false);
91
+ return (
92
+ <Popover open={open} onOpenChange={setOpen}>
93
+ <ButtonGroup variant="filled">
94
+ <ButtonGroup.Primary onClick={() => save()}>
95
+ Salvar
96
+ </ButtonGroup.Primary>
97
+ <PopoverTrigger asChild>
98
+ <ButtonGroup.Chevron aria-label="Opções de salvamento" />
99
+ </PopoverTrigger>
100
+ </ButtonGroup>
101
+ <PopoverContent align="end">
102
+ <button onClick={() => saveAndClose()}>Salvar e fechar</button>
103
+ <button onClick={() => saveAsTemplate()}>Salvar como template</button>
104
+ </PopoverContent>
105
+ </Popover>
106
+ );
107
+ }
108
+ ```
109
+
110
+ ### 2. Variants
111
+
112
+ ```tsx
113
+ {/* Outline secondary (estilo "Follow" do print) */}
114
+ <ButtonGroup color="secondary" variant="outline" size="sm">
115
+ <ButtonGroup.Primary onClick={follow}>Follow</ButtonGroup.Primary>
116
+ <ButtonGroup.Chevron onClick={openMenu} aria-label="Mais ações" />
117
+ </ButtonGroup>
118
+
119
+ {/* Critical filled */}
120
+ <ButtonGroup color="critical" variant="filled">
121
+ <ButtonGroup.Primary onClick={deleteItem}>Deletar</ButtonGroup.Primary>
122
+ <ButtonGroup.Chevron onClick={openMenu} aria-label="Outras opções de deletar" />
123
+ </ButtonGroup>
124
+
125
+ {/* Override individual: primary=brand, chevron=secondary */}
126
+ <ButtonGroup variant="filled">
127
+ <ButtonGroup.Primary color="primary">Salvar</ButtonGroup.Primary>
128
+ <ButtonGroup.Chevron color="secondary" aria-label="Mais" />
129
+ </ButtonGroup>
130
+ ```
131
+
132
+ ### 3. Disabled / loading
133
+
134
+ ```tsx
135
+ {/* Group inteiro desabilitado */}
136
+ <ButtonGroup disabled>
137
+ <ButtonGroup.Primary>Salvar</ButtonGroup.Primary>
138
+ <ButtonGroup.Chevron aria-label="..." />
139
+ </ButtonGroup>
140
+
141
+ {/* Só o primary disabled (chevron continua clicável) */}
142
+ <ButtonGroup>
143
+ <ButtonGroup.Primary disabled>Salvar</ButtonGroup.Primary>
144
+ <ButtonGroup.Chevron aria-label="Outras opções" />
145
+ </ButtonGroup>
146
+
147
+ {/* Loading no primary */}
148
+ <ButtonGroup>
149
+ <ButtonGroup.Primary loading>Salvando</ButtonGroup.Primary>
150
+ <ButtonGroup.Chevron aria-label="..." disabled />
151
+ </ButtonGroup>
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Gotchas
157
+
158
+ - **`aria-label` é obrigatório** no `<ButtonGroup.Chevron>` — TypeScript reclama se omitir. Icon-only sem label viola WCAG 4.1.2.
159
+ - **`shape="pill"` não funciona** com ButtonGroup — radius interno é forçado retangular pra encostar os 2 slots. Pra pill, use `<Button>` standalone.
160
+ - **Icon sizes (`icon-*`)** não suportadas no group — o Chevron já é quadrado com dimensão derivada da size do group. Se quiser group "icon-only", use 2 `<Button>` standalone.
161
+ - **Border duplicado entre slots** — colapsado via `-ml-px` no Chevron. Funciona em filled/soft/ghost. Em **outline**, o efeito visual pode ter 1px de overlap; teste no contexto antes de promover.
162
+ - **Override de `size` em slot individual** quebra alinhamento vertical — passe `size` só no group, não nos slots.