@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.
- 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/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/global/componentes.md +217 -0
- package/dist-lib/ai/global/composicao.md +182 -0
- package/dist-lib/ai/indice.json +177 -0
- package/dist-lib/ai/lint/ds-lint-patterns.mjs +115 -0
- package/dist-lib/ai/manifest.json +16 -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/dashboard/blueprint.md +47 -0
- package/dist-lib/ai/roteiros/dashboard/entrevista.md +62 -0
- package/dist-lib/ai/roteiros/dashboard/geracao.md +88 -0
- package/dist-lib/ai/roteiros/dashboard/roteiro.md +88 -0
- 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.
|