@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,99 @@
|
|
|
1
|
+
# CardCheckbox
|
|
2
|
+
|
|
3
|
+
**Categoria:** Form input (opção destacada).
|
|
4
|
+
**Status:** Disponível v0.7.0+.
|
|
5
|
+
|
|
6
|
+
## O que é
|
|
7
|
+
|
|
8
|
+
Checkbox apresentado como card clicável grande, com label + description
|
|
9
|
+
visíveis. Mesma estética dos radio cards do design system (bg verde fraco no
|
|
10
|
+
selected, border-brand, shadow leve).
|
|
11
|
+
|
|
12
|
+
Diferente do `FormFieldCheckbox` que é layout horizontal compact (ideal pra
|
|
13
|
+
um aceite "li os termos") — `CardCheckbox` é pra opções que merecem destaque
|
|
14
|
+
visual ("salvar essa conta", "marcar como favorito", "ativar notificações").
|
|
15
|
+
|
|
16
|
+
## Quando usar
|
|
17
|
+
|
|
18
|
+
- Opção dentro de form que precisa de visibilidade extra (com explicação)
|
|
19
|
+
- Toggle de feature opcional após uma ação principal (ex: "salvar essa conta
|
|
20
|
+
pra usar depois")
|
|
21
|
+
- Item de lista de opções binárias onde cada item tem título + descrição
|
|
22
|
+
- Quando você quer área-clique grande (acessibilidade) sem inflar visualmente
|
|
23
|
+
um checkbox simples
|
|
24
|
+
|
|
25
|
+
## Quando NÃO usar
|
|
26
|
+
|
|
27
|
+
- Lista de aceite legal (terms & conditions) → use `FormFieldCheckbox`
|
|
28
|
+
- Múltiplos checkboxes em sequência (form denso) → use `FormFieldCheckbox` com
|
|
29
|
+
`gap-form-gap`
|
|
30
|
+
- Toggle ON/OFF de uma config (sem descrição longa) → use `Switch`
|
|
31
|
+
|
|
32
|
+
## Props essenciais
|
|
33
|
+
|
|
34
|
+
| Prop | Tipo | Default | Descrição |
|
|
35
|
+
|-------------------|-------------------------|---------|----------------------------------------------|
|
|
36
|
+
| `label` | `ReactNode` | — | Título do card (obrigatório) |
|
|
37
|
+
| `description` | `ReactNode` | — | Texto secundário abaixo do label |
|
|
38
|
+
| `icon` | `ReactNode` | — | Ícone opcional à esquerda (antes do checkbox)|
|
|
39
|
+
| `checked` | `boolean \| "indeterminate"` | — | Estado controlado |
|
|
40
|
+
| `onCheckedChange` | `(checked) => void` | — | Callback de mudança |
|
|
41
|
+
| `disabled` | `boolean` | `false` | Desativa o card inteiro |
|
|
42
|
+
| `className` | `string` | — | Override de classe no card root |
|
|
43
|
+
|
|
44
|
+
Aceita também as demais props do `CheckboxPrimitive.Root` do Radix
|
|
45
|
+
(`name`, `required`, etc), **exceto `id`** (gerado automaticamente via
|
|
46
|
+
`useId()` pra vincular o `<label>` ao checkbox) e `className`, que é
|
|
47
|
+
aplicado ao card root (`<label>`), não ao checkbox Radix.
|
|
48
|
+
|
|
49
|
+
## Exemplo mínimo
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { useState } from "react";
|
|
53
|
+
import { CardCheckbox } from "@/components/ui/CardCheckbox";
|
|
54
|
+
|
|
55
|
+
function MyForm() {
|
|
56
|
+
const [save, setSave] = useState(true);
|
|
57
|
+
return (
|
|
58
|
+
<CardCheckbox
|
|
59
|
+
label="Salvar essa conta pra usar depois"
|
|
60
|
+
description="A conta aparecerá nas próximas vezes em 'Contas cadastradas'."
|
|
61
|
+
checked={save}
|
|
62
|
+
onCheckedChange={(v) => setSave(v === true)}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Variants (via card-checkbox.styles.ts)
|
|
69
|
+
|
|
70
|
+
| Variant | Valores | Efeito |
|
|
71
|
+
|-----------|--------------------------|---------------------------------------------------|
|
|
72
|
+
| `selected` | `true` \| `false` | Selected: bg-success-muted + border-brand + shadow|
|
|
73
|
+
| `disabled` | `true` | Opacity 50% + pointer-events none |
|
|
74
|
+
|
|
75
|
+
`selected` é derivado de `checked` automaticamente — não precisa passar.
|
|
76
|
+
|
|
77
|
+
## Gotchas
|
|
78
|
+
|
|
79
|
+
- **Não usar dentro de `<form>` sem prevenir submit** — clicar no card
|
|
80
|
+
dispara o checkbox via label htmlFor. Se houver `<button type="submit">`
|
|
81
|
+
no mesmo form, o teclado Enter no card pode submeter. Use
|
|
82
|
+
`<button type="button">` em outros controles próximos.
|
|
83
|
+
- **`onCheckedChange` recebe `boolean | "indeterminate"`** — não pode passar
|
|
84
|
+
direto pra `setState<boolean>`. Use `(v) => setX(v === true)`.
|
|
85
|
+
- **icon prop** é renderizado dentro de um `<span aria-hidden>` — não use
|
|
86
|
+
ícone como controle clicável, só como dica visual.
|
|
87
|
+
|
|
88
|
+
## Tokens usados
|
|
89
|
+
|
|
90
|
+
- `bg-bg-success-muted` (selected bg)
|
|
91
|
+
- `border-border-brand` (selected border)
|
|
92
|
+
- `bg-bg-muted` (hover bg, não-selected)
|
|
93
|
+
- `text-fg-default` (label)
|
|
94
|
+
- `text-fg-muted` (description)
|
|
95
|
+
- `shadow-sh-sm` (selected shadow)
|
|
96
|
+
- `rounded-radius-lg`
|
|
97
|
+
- `p-pad-xl`
|
|
98
|
+
- `gap-gp-lg` (entre checkbox/icon/body)
|
|
99
|
+
- `ring-ring-brand` (focus)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# CardOption
|
|
2
|
+
|
|
3
|
+
<!-- ds:regras
|
|
4
|
+
- switch = efeito IMEDIATO, sem Salvar, e só em `layout="list"`; tela com botão Salvar → `type="checkbox"`
|
|
5
|
+
- opções que se COMPARAM (plano, tier, preço, descrição longa) → cards espaçados; itens do mesmo tipo com rótulo curto (settings, permissões, pagamento, endereço) → `layout="list"`
|
|
6
|
+
- mais de ~5 opções → não é CardOption: `Select`/`Combobox`. E on/off de UMA coisa nunca são 2 radios
|
|
7
|
+
- `type="radio"` EXIGE `<CardOptionGroup type="radio">` em volta; checkbox e switch funcionam soltos
|
|
8
|
+
- em lista o selecionado NÃO pinta por default (a borda ali é a divisória) — queira pintado? `highlightSelected` no grupo
|
|
9
|
+
- omita `orientation`, `highlightSelected` e `size`: derivam do type · `md` é o calibrado
|
|
10
|
+
-->
|
|
11
|
+
|
|
12
|
+
**O que é** — controle de formulário apresentado como **card clicável** (área grande, label +
|
|
13
|
+
descrição visíveis), com o controle trocável por prop: `checkbox`, `radio` ou `switch`.
|
|
14
|
+
**Categoria**: Form Controls.
|
|
15
|
+
|
|
16
|
+
**Quando usar** — opção destacada que merece área de clique grande e texto de apoio: escolha
|
|
17
|
+
de plano/frete, lista de permissões, painel de configurações. Para campo compacto em
|
|
18
|
+
formulário, use `FormFieldCheckbox` / `RadioGroup` cru. Para um único toggle inline, `Switch`
|
|
19
|
+
direto. **Acima de ~5 opções, não use CardOption**: vira parede de cards — é `Select` ou
|
|
20
|
+
`Combobox` ([Soul DS](https://soul.emplifi.io/latest/components/in-progress/radio-button-card-Pnx3WsEU)).
|
|
21
|
+
|
|
22
|
+
## Escolhendo: duas perguntas
|
|
23
|
+
|
|
24
|
+
### 1. Qual `type`? — depende da DECISÃO, não da aparência
|
|
25
|
+
|
|
26
|
+
| Use | Quando | Fonte |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `switch` | liga/desliga uma funcionalidade **com efeito imediato**, sem Salvar | [NN/g](https://www.nngroup.com/articles/toggle-switch-guidelines/) |
|
|
29
|
+
| `radio` | uma entre várias mutuamente exclusivas, e o usuário precisa **ver todas** pra decidir | [NN/g](https://www.nngroup.com/articles/checkboxes-vs-radio-buttons/) |
|
|
30
|
+
| `checkbox` | combinação livre (zero, uma ou várias) — **e** o substituto do switch quando há Salvar | idem |
|
|
31
|
+
|
|
32
|
+
Os dois erros que essa tabela evita, os dois de fonte externa:
|
|
33
|
+
|
|
34
|
+
- **Switch em tela com botão Salvar.** O switch promete efeito imediato — "should take
|
|
35
|
+
immediate effect and should not require the user to click Save or Submit" (NN/g). Num form
|
|
36
|
+
com submit, o usuário não sabe se já valeu. Ali o controle é `type="checkbox"`.
|
|
37
|
+
- **Dois radios pra on/off de uma coisa.** Um par "Ativado / Desativado" é um switch (se
|
|
38
|
+
imediato) ou **um** checkbox (se tem submit) — nunca dois radios.
|
|
39
|
+
|
|
40
|
+
### 2. Lista ou cards espaçados? — depende de quanta COMPARAÇÃO a decisão pede
|
|
41
|
+
|
|
42
|
+
| Use | Quando |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `layout="list"` | itens **do mesmo tipo**, rótulo curto, decisão já conhecida: configurações (switch), permissões (checkbox), meio de pagamento / endereço / forma de entrega (radio). Densidade > destaque |
|
|
45
|
+
| `spaced` (default) | cada opção precisa ser **comparada** — preço, descrição longa, ícone, badge: plano, tier, onboarding. O gap é o que separa as unidades de comparação |
|
|
46
|
+
|
|
47
|
+
**Switch vive em lista, não em card solto.** É literal na Apple HIG: *"Use the switch toggle
|
|
48
|
+
style only in a list row"* — e a razão é que o switch tem mais peso visual que um checkbox, o
|
|
49
|
+
que só se justifica quando a linha inteira lhe dá contexto
|
|
50
|
+
([HIG](https://developers.apple.com/design/human-interface-guidelines/components/selection-and-input/toggles/)).
|
|
51
|
+
|
|
52
|
+
Meio de pagamento é o caso clássico de **radio em lista**: alvo grande e indicador visível
|
|
53
|
+
resolvem o problema do radio nativo, e o logo/ícone entra à esquerda, junto do nome
|
|
54
|
+
([Baymard](https://baymard.com/blog/payment-method-selection)) — o que o `icon` do CardOption
|
|
55
|
+
já faz por construção.
|
|
56
|
+
|
|
57
|
+
## Props essenciais
|
|
58
|
+
|
|
59
|
+
| Prop | Tipo | Default | Descrição |
|
|
60
|
+
|------|------|---------|-----------|
|
|
61
|
+
| `type` | `"checkbox" \| "radio" \| "switch"` | herda do grupo, ou `"checkbox"` | qual controle o card embrulha |
|
|
62
|
+
| `value` | `string` | — | **obrigatório com `type="radio"`** |
|
|
63
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | 8 / 12 / 16px de padding |
|
|
64
|
+
| `orientation` | `"left" \| "right"` | **derivado do `type`** | lado do controle |
|
|
65
|
+
| `highlightSelected` | `boolean` | do `type`; **em lista: `false`** | fundo + cor de borda no selecionado |
|
|
66
|
+
| `label` | `ReactNode` | — | obrigatório |
|
|
67
|
+
| `description` | `ReactNode` | — | texto de apoio |
|
|
68
|
+
| `icon` | `ReactNode` | — | **sempre à esquerda**, mesmo com `orientation="right"`. Piso de 20×20px |
|
|
69
|
+
| `checked` / `onCheckedChange` | | — | controlado (no radio, quem manda é o grupo) |
|
|
70
|
+
| `disabled` | `boolean` | — | |
|
|
71
|
+
|
|
72
|
+
### `CardOptionGroup`
|
|
73
|
+
|
|
74
|
+
| Prop | Tipo | Default | Descrição |
|
|
75
|
+
|------|------|---------|-----------|
|
|
76
|
+
| `type` | igual ao item | `"checkbox"` | `"radio"` faz o grupo **ser** o `RadioGroup` do Radix |
|
|
77
|
+
| `layout` | `"spaced" \| "list"` | `"spaced"` | `list` = contorno no grupo + divisória entre linhas, sem gap. Vale pros **3 tipos** |
|
|
78
|
+
| `highlightSelected` | `boolean` | — | liga/desliga o destaque em todos os filhos de uma vez |
|
|
79
|
+
| `size` / `orientation` | | | aplicados a todos os filhos |
|
|
80
|
+
| `value` / `defaultValue` / `onValueChange` / `name` | | — | só `type="radio"` |
|
|
81
|
+
|
|
82
|
+
## Exemplo mínimo
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// checkbox solto
|
|
86
|
+
<CardOption
|
|
87
|
+
label="Salvar essa conta pra usar depois"
|
|
88
|
+
description="Aparece na lista de contas favoritas"
|
|
89
|
+
checked={salvar}
|
|
90
|
+
onCheckedChange={setSalvar}
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
// radio — o grupo é obrigatório
|
|
94
|
+
<CardOptionGroup type="radio" value={frete} onValueChange={setFrete}>
|
|
95
|
+
<CardOption value="standard" label="Standard" description="4 a 10 dias úteis" />
|
|
96
|
+
<CardOption value="express" label="Express" description="2 a 3 dias úteis" />
|
|
97
|
+
</CardOptionGroup>
|
|
98
|
+
|
|
99
|
+
// lista de settings — switch, à direita, sem destaque de selecionado
|
|
100
|
+
<CardOptionGroup type="switch" layout="list">
|
|
101
|
+
<CardOption label="Wi-Fi" description="Conectar a redes sem fio" checked={wifi} onCheckedChange={setWifi} />
|
|
102
|
+
<CardOption label="Bluetooth" description="Permitir conexões Bluetooth" checked={bt} onCheckedChange={setBt} />
|
|
103
|
+
</CardOptionGroup>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Gotchas / cuidados
|
|
107
|
+
|
|
108
|
+
- **`type="radio"` sem `CardOptionGroup` não funciona direito.** É o `RadioGroup` do Radix que
|
|
109
|
+
dá navegação por seta e agrupamento por `name`. Checkbox e switch são autônomos.
|
|
110
|
+
- **Não passe `orientation` nem `highlightSelected` sem motivo.** Eles derivam do `type`, e o
|
|
111
|
+
default é a convenção: switch fica **à direita** (linha de configuração) e **sem destaque de
|
|
112
|
+
selecionado**, porque switch é *estado*, não seleção — uma lista de settings toda pintada de
|
|
113
|
+
verde é ruído. Foi por isso que o exemplo antigo do Card Toggle não tinha estado visual.
|
|
114
|
+
- **`layout="list"` não é exclusivo do switch.** Com radio vira seletor de linha única; com
|
|
115
|
+
checkbox, lista de permissões. No modo lista o contorno é do grupo e a divisória é a borda
|
|
116
|
+
de baixo de cada item (a última suprimida) — não force borda no item.
|
|
117
|
+
- **Em lista, o destaque de selecionado vem desligado — inclusive em checkbox e radio.** É
|
|
118
|
+
consequência de onde a borda mora: em lista a única borda do item é a de baixo, ou seja a
|
|
119
|
+
**divisória**, então o `border-brand` não contorna o selecionado — pinta a linha que o separa
|
|
120
|
+
do vizinho — e o fundo vira faixa colorida no meio da lista (medido em 2026-08-27, antes do
|
|
121
|
+
ajuste: linha verde com divisória verde). Quem quer o pintado liga `highlightSelected` no
|
|
122
|
+
grupo ou no item; a prop vence nas duas direções, e em lista a sombra do destaque sai (dentro
|
|
123
|
+
do `overflow-hidden` do grupo ela não eleva, só vaza).
|
|
124
|
+
- **O ícone fica sempre à esquerda**, inclusive com `orientation="right"`: o `order-last` move
|
|
125
|
+
só o controle. Ele identifica a opção e pertence ao lado do texto. Piso de 20×20px.
|
|
126
|
+
- **É um `<label htmlFor>` nativo, nunca `<button>` (L-025).** Não embrulhe em outro botão nem
|
|
127
|
+
ponha `onClick` no card: o clique já chega ao controle real pelo label, e trocar isso quebra
|
|
128
|
+
o leitor de tela ("button" em vez de checkbox) e o submit nativo.
|
|
129
|
+
- **O anel de foco é do CARD**, via `has-[:focus-visible]`. O `CardCheckbox` antigo declarava
|
|
130
|
+
`focus-visible:ring-4` no próprio `<label>` — inerte, porque label não recebe foco (medido
|
|
131
|
+
em 2026-08-27: o único anel visível era o do controle de 16px).
|
|
132
|
+
- **`CardCheckbox` continua existindo** como atalho de `type="checkbox"`. Componente novo
|
|
133
|
+
deve usar `CardOption`.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Chart — USAGE
|
|
2
|
+
|
|
3
|
+
Wrapper sobre o **Recharts 3** que aplica a paleta do DS (tokens `--color-chart-1..5`) e fornece Tooltip/Legend já estilizados com os tokens iGreen. Categoria: data-display.
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
|
|
7
|
+
- Qualquer gráfico (área, barras, linhas, pizza, radar, radial) numa tela do iGreen
|
|
8
|
+
- Garantir cores, tooltip e legenda consistentes entre gráficos
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import {
|
|
14
|
+
ChartContainer,
|
|
15
|
+
ChartTooltip,
|
|
16
|
+
ChartTooltipContent,
|
|
17
|
+
ChartLegend,
|
|
18
|
+
ChartLegendContent,
|
|
19
|
+
type ChartConfig,
|
|
20
|
+
} from "@/components/ui/Chart";
|
|
21
|
+
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Conceito
|
|
25
|
+
|
|
26
|
+
`ChartContainer` recebe um `config` (mapa série → label/cor/ícone) e injeta cada
|
|
27
|
+
cor como CSS var `--color-{key}` **escopada nessa instância**. Os elementos do
|
|
28
|
+
Recharts referenciam `var(--color-{key})`.
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
const config = {
|
|
32
|
+
desktop: { label: "Desktop", color: "var(--color-chart-1)" },
|
|
33
|
+
mobile: { label: "Mobile", color: "var(--color-chart-2)" },
|
|
34
|
+
} satisfies ChartConfig;
|
|
35
|
+
|
|
36
|
+
<ChartContainer config={config} className="h-[260px] w-full">
|
|
37
|
+
<AreaChart data={data}>
|
|
38
|
+
<CartesianGrid vertical={false} />
|
|
39
|
+
<XAxis dataKey="month" tickLine={false} axisLine={false} />
|
|
40
|
+
<ChartTooltip content={<ChartTooltipContent />} />
|
|
41
|
+
<Area dataKey="desktop" fill="var(--color-desktop)" stroke="var(--color-desktop)" />
|
|
42
|
+
<Area dataKey="mobile" fill="var(--color-mobile)" stroke="var(--color-mobile)" />
|
|
43
|
+
</AreaChart>
|
|
44
|
+
</ChartContainer>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Props essenciais
|
|
48
|
+
|
|
49
|
+
| Componente | Prop | Função |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `ChartContainer` | `config: ChartConfig` | mapa série → `{ label, color?, icon?, theme? }` |
|
|
52
|
+
| `ChartContainer` | `className` | controle de altura/aspect (ex: `h-[260px] w-full`) |
|
|
53
|
+
| `ChartTooltip` | `content={<ChartTooltipContent/>}` | tooltip do DS |
|
|
54
|
+
| `ChartTooltipContent` | `hideLabel`, `hideIndicator`, `indicator` (`dot`/`line`/`dashed`), `labelFormatter`, `formatter`, `nameKey`, `labelKey` | customização |
|
|
55
|
+
| `ChartLegend` | `content={<ChartLegendContent/>}` | legenda do DS |
|
|
56
|
+
|
|
57
|
+
## Cores
|
|
58
|
+
|
|
59
|
+
Use os tokens `var(--color-chart-1)` … `var(--color-chart-5)` no `config`.
|
|
60
|
+
Paleta verde-marca + harmônicas (teal/azul/âmbar/violeta), light/dark-aware.
|
|
61
|
+
|
|
62
|
+
## Grid (linhas-guia)
|
|
63
|
+
|
|
64
|
+
`<CartesianGrid vertical={false} strokeDasharray="4 4" />` — **sem** passar
|
|
65
|
+
`stroke`. O container já reescreve o stroke default pro token `--color-chart-grid`
|
|
66
|
+
(visível em light e dark). Mesmo vale pro `PolarGrid` (radar/radial).
|
|
67
|
+
|
|
68
|
+
## Gotchas
|
|
69
|
+
|
|
70
|
+
- `ChartContainer` já embute o `ResponsiveContainer` — passe só o gráfico (ex:
|
|
71
|
+
`<AreaChart>`) como filho, **sem** envolver em outro ResponsiveContainer.
|
|
72
|
+
- Defina **altura** via `className` (ex: `h-[260px]`) — o aspect default é `video`.
|
|
73
|
+
- Recharts 3: `dataKey` precisa casar com a chave do `config` pra `--color-{key}`
|
|
74
|
+
resolver.
|
|
75
|
+
|
|
76
|
+
### Recharts 3 — caveats que quebram silenciosamente
|
|
77
|
+
|
|
78
|
+
- **`text-display-sm` / `text-display-xs` NÃO existem** como utility (renderizam
|
|
79
|
+
14px). Em KPIs use `text-heading-sm` (24–32px) / `text-heading-xs` (24px) ou
|
|
80
|
+
`text-display-md` (28–39px).
|
|
81
|
+
- **Pizza**: não há `activeIndex`/`activeShape` no `Pie` — usar a prop
|
|
82
|
+
`shape={(props, index) => <Sector .../>}` (render por setor).
|
|
83
|
+
- **Radial empilhado**: precisa de `<PolarAngleAxis type="number" domain={[0,total]} />`
|
|
84
|
+
senão só 1 segmento aparece.
|
|
85
|
+
- **Eixo Y omite ticks de borda** (ex: o `0`): `interval={0}` força todos.
|
|
86
|
+
- **Linha-guia duplicada no topo**: o `domain` máximo deve ser igual ao maior
|
|
87
|
+
`tick` (ex: `domain={[0,90]}` com tick 90, não `[0,95]`).
|
|
88
|
+
|
|
89
|
+
## Composições de dashboard
|
|
90
|
+
|
|
91
|
+
Catálogo + padrões de header/card/categoria/largura na rota `#/chart-showcase` do
|
|
92
|
+
catálogo hospedado. (Fonte no repo do DS: `.ai/context/components/chart-patterns.md`
|
|
93
|
+
— caminho **interno**, não existe em quem consome por npm ou copy-in.)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Chip — USAGE
|
|
2
|
+
|
|
3
|
+
Pílula compacta para status, tags, filtros — dual-mode (span estático ou button interativo).
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
- Tags / categorias (status, prioridade, label)
|
|
7
|
+
- Filtros aplicados (com onClick = remove)
|
|
8
|
+
- Chips de seleção (selected state)
|
|
9
|
+
- Counters inline
|
|
10
|
+
|
|
11
|
+
## Import
|
|
12
|
+
```tsx
|
|
13
|
+
import { Chip, ChipGroup, ChipGroupItem } from "@/components/ui/Chip";
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Variants
|
|
17
|
+
| Variant | Valores | Default | Quando |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| `color` | primary / neutral / danger / warning / success / info | neutral | Cor semântica |
|
|
20
|
+
| `variant` | solid / outline / soft / soft-outline | soft | Intensidade visual |
|
|
21
|
+
| `size` | sm / md / lg / xl | md | sm=24px, md=28px, lg=32px, xl=36px |
|
|
22
|
+
| `shape` | pill / rounded | pill | Border-radius |
|
|
23
|
+
|
|
24
|
+
## Props essenciais
|
|
25
|
+
| Prop | Tipo | Função |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `onClick` | () => void | Vira `<button>` automaticamente |
|
|
28
|
+
| `asButton` | boolean | Força modo button mesmo sem onClick |
|
|
29
|
+
| `selected` | boolean | Visual ativo (force soft) |
|
|
30
|
+
|
|
31
|
+
## ChipGroup props
|
|
32
|
+
Seleção via `@radix-ui/react-toggle-group`. Visual ativo/inativo configurável independentemente.
|
|
33
|
+
|
|
34
|
+
| Prop | Tipo | Default | Função |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| `type` | `"single"` \| `"multiple"` | — (obrigatória) | single = radio-like, multiple = checkbox-like |
|
|
37
|
+
| `value` | string (single) / string[] (multiple) | — | Seleção controlada |
|
|
38
|
+
| `defaultValue` | string (single) / string[] (multiple) | — | Seleção inicial (uncontrolled) |
|
|
39
|
+
| `onValueChange` | (value: string) => void (single) / (value: string[]) => void (multiple) | — | Callback de mudança |
|
|
40
|
+
| `inactiveColor` | mesmos valores de `color` | `"neutral"` | Cor dos items NÃO selecionados |
|
|
41
|
+
| `inactiveVariant` | mesmos valores de `variant` | `"outline"` | Variant dos items NÃO selecionados |
|
|
42
|
+
| `activeColor` | mesmos valores de `color` | `"primary"` | Cor dos items selecionados |
|
|
43
|
+
| `activeVariant` | mesmos valores de `variant` | `"soft-outline"` | Variant dos items selecionados |
|
|
44
|
+
| `size` | sm / md / lg / xl | `"md"` | Size aplicado em todos os chips do grupo |
|
|
45
|
+
| `shape` | pill / rounded | `"pill"` | Shape aplicado em todos os chips |
|
|
46
|
+
| `orientation` | `"horizontal"` \| `"vertical"` | `"horizontal"` | Direção do grupo |
|
|
47
|
+
| `ariaLabel` | string | — | Aria-label do grupo |
|
|
48
|
+
|
|
49
|
+
## Exemplo mínimo
|
|
50
|
+
```tsx
|
|
51
|
+
// Tag estática
|
|
52
|
+
<Chip color="warning" variant="soft">Royal</Chip>
|
|
53
|
+
|
|
54
|
+
// Filtro com remove
|
|
55
|
+
<Chip color="primary" onClick={removeFilter}>Status: Ativo ×</Chip>
|
|
56
|
+
|
|
57
|
+
// Group multi-select
|
|
58
|
+
<ChipGroup type="multiple" value={selected} onValueChange={setSelected}>
|
|
59
|
+
<ChipGroupItem value="all">Todas</ChipGroupItem>
|
|
60
|
+
<ChipGroupItem value="unread">Não lidas</ChipGroupItem>
|
|
61
|
+
</ChipGroup>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Cuidados / Gotchas
|
|
65
|
+
- Sem `onClick` e sem `asButton={true}` renderiza como `<span>` (decorativo)
|
|
66
|
+
- O visual interativo (cursor pointer + focus ring) é calculado internamente a partir de `onClick`/`asButton` — não existe prop `interactive` no Chip
|
|
67
|
+
- `selected=true` força visual "soft" da cor mesmo se `variant="outline"`
|
|
68
|
+
- Pra counter inline, usar `chipCount` slot (10px font-semibold)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# ChoroplethMap
|
|
2
|
+
|
|
3
|
+
**Categoria:** iGreen (tv() + d3-geo). Primitiva **genérica** de mapa coroplético (regiões SVG coloridas por valor).
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
|
|
7
|
+
- Visualizar uma métrica por região geográfica: municípios (IBGE), estados, países.
|
|
8
|
+
- Ex.: "Mapa de Cidades" do Rankings — clientes por município do Brasil.
|
|
9
|
+
|
|
10
|
+
Não é acoplado a nenhum dataset: você passa a geografia (GeoJSON/TopoJSON) + um mapa `id → número`.
|
|
11
|
+
|
|
12
|
+
## Props essenciais
|
|
13
|
+
|
|
14
|
+
| Prop | Tipo | Default | Descrição |
|
|
15
|
+
|------|------|---------|-----------|
|
|
16
|
+
| `geography` | `FeatureCollection \| Feature[] \| Topology` | — | Fonte geográfica. TopoJSON com 2+ objetos exige `topologyObject`. |
|
|
17
|
+
| `topologyObject` | `string` | objeto único da `Topology` | Nome do objeto a extrair do `Topology` (ex.: `"municipios"`). Obrigatório só quando há 2+ objetos — com 2+ e sem a prop, o mapa renderiza **vazio** (sem erro). |
|
|
18
|
+
| `values` | `Record<string \| number, number>` | — | Mapa `id → valor`. Ids casam com `getFeatureId`. |
|
|
19
|
+
| `getFeatureId` | `(f) => string \| number` | `f.id` → `f.properties.id` | Como obter o id de uma feature. |
|
|
20
|
+
| `getFeatureName` | `(f) => string` | `properties.name/nome/NOME` → id | Nome exibido no tooltip. |
|
|
21
|
+
| `colorScale` | `(value, {min,max}) => string` | gradiente token | Escala custom (retorna cor CSS). |
|
|
22
|
+
| `scaleToken` | `"brand"\|"success"\|"info"\|"warning"\|"danger"` | `"brand"` | Token DS do extremo "cheio" do gradiente default. |
|
|
23
|
+
| `domain` | `[number, number]` | min/max de `values` | Domínio fixo da escala. |
|
|
24
|
+
| `projection` | `GeoProjection` (d3-geo) | `geoMercator().fitSize(...)` | Projeção custom (senão auto-fit). |
|
|
25
|
+
| `width` / `height` | `number` | `800` / `600` | ViewBox (o SVG é responsivo, `w-full h-auto`). |
|
|
26
|
+
| `strokeWidth` | `number` | `0.5` | Espessura das divisas (unidades do viewBox). |
|
|
27
|
+
| `showLegend` | `boolean` | `true` | Barra de gradiente + min/max. |
|
|
28
|
+
| `legendTitle` | `ReactNode` | — | Título da legenda. |
|
|
29
|
+
| `formatValue` | `(v) => string` | `Intl.NumberFormat("pt-BR")` | Formata legenda + tooltip. |
|
|
30
|
+
| `renderTooltip` | `(info) => ReactNode` | nome + valor | Conteúdo custom do tooltip. |
|
|
31
|
+
| `onFeatureClick` | `(info) => void` | — | Clique numa região. |
|
|
32
|
+
| `selectedId` | `string \| number \| null` | — | Destaque PERSISTENTE de uma região (controlado; par natural de `onFeatureClick` pra seleção por clique). |
|
|
33
|
+
| `ariaLabel` | `string` | `"Mapa"` | Rótulo acessível (`role="img"`). |
|
|
34
|
+
|
|
35
|
+
## Exemplo mínimo (municípios do Brasil, IBGE TopoJSON)
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { ChoroplethMap } from "@snksergio/design-system";
|
|
39
|
+
import brasilMunicipios from "./geo/br-municipios.topo.json"; // TopoJSON IBGE
|
|
40
|
+
|
|
41
|
+
<ChoroplethMap
|
|
42
|
+
geography={brasilMunicipios}
|
|
43
|
+
topologyObject="municipios"
|
|
44
|
+
values={clientesPorMunicipio} // { "3550308": 120, "3304557": 88, ... }
|
|
45
|
+
getFeatureId={(f) => f.id ?? f.properties?.codarea}
|
|
46
|
+
getFeatureName={(f) => f.properties?.name}
|
|
47
|
+
scaleToken="brand"
|
|
48
|
+
legendTitle="Clientes por cidade"
|
|
49
|
+
ariaLabel="Clientes por município"
|
|
50
|
+
/>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Interação (comportamento embutido — não reimplemente)
|
|
54
|
+
|
|
55
|
+
- **Hover**: a região sob o cursor é REDESENHADA por cima de todas (contorno
|
|
56
|
+
`fg-{scaleToken}` com 3× a espessura das divisas + tinta de 18% do token).
|
|
57
|
+
O contorno acompanha a família de cor do mapa — amarelo forte num mapa
|
|
58
|
+
`warning`, roxo forte num `info` — nunca verde fixo.
|
|
59
|
+
- **Tooltip**: próprio (NÃO Radix), vive numa camada `pointer-events-none` e
|
|
60
|
+
SEGUE o cursor, trocando só o conteúdo. Nunca captura o mouse (era a causa
|
|
61
|
+
de flicker direcional com o Tooltip portalado). Flip automático perto das
|
|
62
|
+
bordas. Região sem valor mostra "Sem dados".
|
|
63
|
+
- **Seleção** (`selectedId`, controlado): destaque persistente com a mesma
|
|
64
|
+
técnica do hover, tinta mais forte (32% vs 18%). Toggle é responsabilidade
|
|
65
|
+
do consumer: `onFeatureClick={(i) => setSel(s => s?.id === i.id ? null : i)}`.
|
|
66
|
+
|
|
67
|
+
## Receita: UFs do Brasil (malha IBGE em runtime)
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
const IBGE_UF = "https://servicodados.ibge.gov.br/api/v3/malhas/paises/BR?formato=application/json&qualidade=minima&intrarregiao=UF";
|
|
71
|
+
// devolve TopoJSON { objects: { BRUF } } — objeto único, extraído automaticamente
|
|
72
|
+
// qualidade=minima NÃO traz nome — só properties.codarea. Mapeie código → nome:
|
|
73
|
+
<ChoroplethMap
|
|
74
|
+
geography={geo} // fetch da URL acima
|
|
75
|
+
topologyObject="BRUF" // explícito por clareza (opcional: é único)
|
|
76
|
+
values={clientesPorUf} // { "35": 612, "31": 388, ... } (código IBGE)
|
|
77
|
+
getFeatureId={(f) => String((f.properties as { codarea?: string })?.codarea ?? f.id ?? "")}
|
|
78
|
+
getFeatureName={(f) => UF_NOMES[id] ?? id} // tabela código → "São Paulo"...
|
|
79
|
+
/>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Receita: master-detail (mapa + painel de detalhe)
|
|
83
|
+
|
|
84
|
+
Espelhe a seção "Seleção por clique" da doc page (`ChoroplethMapDoc.tsx`):
|
|
85
|
+
|
|
86
|
+
- Wrapper `relative w-full` com o mapa em largura total; painel FLUTUANDO no
|
|
87
|
+
canto superior direito (área de oceano): `md:absolute md:right-0 md:top-0
|
|
88
|
+
md:w-[220px] md:shadow-sh-md` + card `rounded-radius-lg border
|
|
89
|
+
border-border-default bg-bg-surface p-pad-xl`. Mobile: empilha (`mt-gp-2xl`).
|
|
90
|
+
- ⚠️ **Largura do painel SEMPRE fixa** — painel que cresce com o conteúdo
|
|
91
|
+
redimensiona o svg (que é `w-full` do espaço restante) a cada seleção.
|
|
92
|
+
- Dentro: título + subtítulo num card interno `rounded-radius-md bg-bg-muted
|
|
93
|
+
px-pad-xl py-pad-lg` (`subtle` sobre `surface` mal aparece; `muted` dá o
|
|
94
|
+
contraste do header); métricas abaixo, UMA por linha (`flex justify-between`,
|
|
95
|
+
label `text-caption-md text-fg-muted`, valor `text-body-sm font-semibold
|
|
96
|
+
tabular-nums`).
|
|
97
|
+
- Cobertura parcial: UF fora de `values` fica neutra (`bg-bg-muted`) — estado
|
|
98
|
+
"vazio" de graça, sem prop.
|
|
99
|
+
|
|
100
|
+
## Gotchas
|
|
101
|
+
|
|
102
|
+
- **Dependências:** usa `d3-geo` (projeção + path) e `topojson-client` (TopoJSON → features) — as MESMAS primitivas que a `react-simple-maps` embrulha. Não usamos `react-simple-maps` porque ela trava peer em React ≤18 (o DS é React 19) — usar `--legacy-peer-deps` seria hack e contaminaria a árvore de todos os consumidores.
|
|
103
|
+
- **Cor data-driven é inline:** o `fill` de cada região é `color-mix(... var(--color-bg-{scaleToken}) ...)` e o contorno de hover/seleção é `var(--color-fg-{scaleToken})` (derivados de tokens, valor vem do dado) — não dá pra virar classe utilitária (valor contínuo/infinito). Mesma exceção justificada do `Avatar.colorHex` (L-027). Todo o resto (shell/legenda/tooltip) é classe token.
|
|
104
|
+
- **Não envolva o tooltip em Radix/portal:** qualquer wrapper portalado captura o mouse e reintroduz o flicker direcional (cursor persegue o tooltip → mouseleave do svg → fecha/reabre em loop).
|
|
105
|
+
- **Ids precisam casar:** as chaves de `values` têm que bater com o retorno de `getFeatureId`. TopoJSON do IBGE costuma trazer o código em `properties.codarea` (malhas v3) ou `feature.id` — confira a fonte e ajuste `getFeatureId`.
|
|
106
|
+
- **Dentro de container flex centrado** (ex.: preview de ExampleSection): dê `w-full` ao wrapper do mapa — sem ele o svg cai na largura intrínseca default (300px).
|
|
107
|
+
- **Performance:** milhares de `<path>` renderizam bem em SVG, mas re-render pesado; passe `values`/`geography` estáveis (memoize no consumer). A geometria (paths) é memoizada por `geography/projection/width/height`.
|
|
108
|
+
|
|
109
|
+
## Quando NÃO usar
|
|
110
|
+
|
|
111
|
+
Mapa **fixo** do Brasil por UF (KPI de dashboard) → use a **receita de paths inline**
|
|
112
|
+
(`#/chart-map`, `_dashboard-brazil-map.ts`): é SVG puro, zero dependência e não busca
|
|
113
|
+
malha em runtime.
|
|
114
|
+
|
|
115
|
+
Este componente existe para o caso **data-driven**: topologia arbitrária (municípios de
|
|
116
|
+
um estado, recortes que mudam), projeção real via `d3-geo` e drill-down por
|
|
117
|
+
`onFeatureClick`. Ele traz `d3-geo` + `topojson-client` junto — não pague esse custo
|
|
118
|
+
para desenhar o Brasil parado.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# ColorPicker
|
|
2
|
+
|
|
3
|
+
**Categoria:** composto (Popover + Input + FormField + Button + Separator). Seletor de cor **hex `#RRGGBB`** controlado, pensado para **Tags** e **Filas**.
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
|
|
7
|
+
- Escolher uma cor de marca/identidade para uma entidade (tag, fila, etiqueta, status custom).
|
|
8
|
+
- Quando o usuário precisa **ver e digitar** o hex E ter atalho para uma **paleta curada**.
|
|
9
|
+
|
|
10
|
+
Não use para escolha semântica (sucesso/erro/aviso) — isso são tokens DS, não cor livre.
|
|
11
|
+
|
|
12
|
+
## Anatomia
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
[ swatch ] [ Input hex (#RRGGBB) ] ← trigger inline (swatch abre o popover)
|
|
16
|
+
▼ (clique no swatch)
|
|
17
|
+
┌─────────────────────────────┐
|
|
18
|
+
│ grid 10-col de presets │ ← swatch selecionado = checkmark (contraste auto)
|
|
19
|
+
│ ───────── Separator ─────── │
|
|
20
|
+
│ Cor personalizada [input] │ ← allowCustomHex (default true)
|
|
21
|
+
│ [ Aplicar ] │
|
|
22
|
+
└─────────────────────────────┘
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Props essenciais
|
|
26
|
+
|
|
27
|
+
| Prop | Tipo | Default | Descrição |
|
|
28
|
+
|------|------|---------|-----------|
|
|
29
|
+
| `value` | `string` | — | **Obrigatório.** Hex controlado `#RRGGBB`. |
|
|
30
|
+
| `onValueChange` | `(hex: string) => void` | — | **Obrigatório.** Recebe sempre `#RRGGBB` maiúsculo normalizado. |
|
|
31
|
+
| `presets` | `string[]` | `DEFAULT_COLOR_PRESETS` | Cores do grid (paleta iGreen + neutras, ~26 hex). |
|
|
32
|
+
| `id` | `string` | auto | id do input hex (linka label externo via `htmlFor`). |
|
|
33
|
+
| `state` | `"default" \| "error" \| "warning" \| "success"` | `"default"` | Colore **só a borda** do trigger. **Não** é color variant. |
|
|
34
|
+
| `size` | `"xxs" \| "xs" \| "sm" \| "md"` | `"md"` | Mesma escala do `Input`, e repassado a ele: `xxs` 28px · `xs` 32px · `sm` 36px · `md` 40px. |
|
|
35
|
+
| `disabled` | `boolean` | `false` | Desabilita o seletor inteiro. |
|
|
36
|
+
| `allowCustomHex` | `boolean` | `true` | Mostra o input hex livre + botão Aplicar no popover. |
|
|
37
|
+
| `placeholder` | `string` | `"#RRGGBB"` | Placeholder dos inputs hex. |
|
|
38
|
+
| `open` / `onOpenChange` | — | — | Controle externo de abertura do popover. |
|
|
39
|
+
| `className` | `string` | — | className do container (root). |
|
|
40
|
+
|
|
41
|
+
## Exemplo mínimo
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { ColorPicker } from "@snksergio/design-system";
|
|
45
|
+
|
|
46
|
+
const [color, setColor] = useState("#16A34A");
|
|
47
|
+
|
|
48
|
+
<FormField label="Cor da tag" id="tag-color">
|
|
49
|
+
{({ id }) => (
|
|
50
|
+
<ColorPicker id={id} value={color} onValueChange={setColor} />
|
|
51
|
+
)}
|
|
52
|
+
</FormField>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Variants
|
|
56
|
+
|
|
57
|
+
| Variant | Valores | Efeito |
|
|
58
|
+
|---------|---------|--------|
|
|
59
|
+
| `size` | `xxs` · `xs` · `sm` · `md` | altura do trigger (swatch + input) — escala do `Input` |
|
|
60
|
+
| `state` | `default` · `error` · `warning` · `success` | cor da borda do swatch |
|
|
61
|
+
| `disabled` | — | último compoundVariant (L-006); desabilita tudo |
|
|
62
|
+
|
|
63
|
+
## Gotchas
|
|
64
|
+
|
|
65
|
+
- **Normalização:** aceita `3` ou `6` dígitos com/sem `#` (`fff`, `#FFF`, `00ff00`) → sempre emite `#RRGGBB` **maiúsculo**. Hex inválido no input inline restaura o `value` atual no blur.
|
|
66
|
+
- **bg dinâmico = exceção L-027:** o fundo do swatch e dos presets vem por `style={{ backgroundColor }}` (cor externa). É a única exceção de hardcode permitida; todo o resto é token DS.
|
|
67
|
+
- **Checkmark com contraste auto:** o preset selecionado usa `getContrastTextColor(hex)` para escolher branco/preto — não cor cega.
|
|
68
|
+
- **Anchor do Popover:** o swatch é `forwardRef` via `PopoverTrigger asChild` (L-021). O `ref` encaminhado vai para o botão do swatch.
|
|
69
|
+
- **Foco:** swatch + presets seguem Padrão 1 (botão, `ring-4 ring-ring-brand`); o input hex herda o foco animado do `Input` do DS (Padrão 2).
|
|
70
|
+
- Para parear visualmente com outros campos de um form, envolva em `<FormField>` (render-prop) — o `state` do ColorPicker espelha o do Input para casar a borda.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Combobox
|
|
2
|
+
|
|
3
|
+
**Categoria:** composto (Popover + Command/cmdk). Select de escolha única com **busca (autocomplete)** e **lista rolável**.
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
|
|
7
|
+
- Lista de opções **longa** onde o usuário precisa **digitar pra achar** (ex.: escolher 1 coluna entre 30, 1 país, 1 cliente).
|
|
8
|
+
- Quando um `Select` simples ficaria alto demais / sem scroll confortável e sem busca.
|
|
9
|
+
|
|
10
|
+
Para listas curtas (≤ ~8 itens) e sem necessidade de busca, prefira `FormFieldSelect` / `Select`.
|
|
11
|
+
|
|
12
|
+
## Props essenciais
|
|
13
|
+
|
|
14
|
+
| Prop | Tipo | Default | Descrição |
|
|
15
|
+
|------|------|---------|-----------|
|
|
16
|
+
| `options` | `ComboboxOption[]` | — | `{ value, label, keywords? }`. Busca casa por `label` + `keywords` (que já inclui `value`). |
|
|
17
|
+
| `value` | `string` | — | Valor selecionado (controlado). |
|
|
18
|
+
| `onValueChange` | `(value: string) => void` | — | Recebe o `value` da opção escolhida. |
|
|
19
|
+
| `placeholder` | `ReactNode` | `"Selecione…"` | Texto do trigger vazio. |
|
|
20
|
+
| `searchPlaceholder` | `string` | `"Buscar…"` | Placeholder do input de busca. |
|
|
21
|
+
| `emptyMessage` | `ReactNode` | `"Nenhum resultado."` | Quando a busca não casa nada. |
|
|
22
|
+
| `open` / `defaultOpen` / `onOpenChange` | — | — | Controle de abertura (igual a um Select). |
|
|
23
|
+
| `align` | `"start" \| "center" \| "end"` | `"start"` | Alinhamento do dropdown. |
|
|
24
|
+
| `className` | `string` | — | Estiliza o **trigger** (aceita os mesmos overrides de um `SelectTrigger`). |
|
|
25
|
+
| `contentClassName` | `string` | — | Estiliza o **dropdown** (PopoverContent). |
|
|
26
|
+
| `disabled` | `boolean` | — | Desabilita o trigger. |
|
|
27
|
+
|
|
28
|
+
`ComboboxOption = { value: string; label: string; keywords?: string[] }`
|
|
29
|
+
|
|
30
|
+
## Exemplo mínimo
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { Combobox } from "@snksergio/design-system";
|
|
34
|
+
|
|
35
|
+
<Combobox
|
|
36
|
+
options={columns.map((c) => ({ value: c.key, label: c.label }))}
|
|
37
|
+
value={field}
|
|
38
|
+
onValueChange={setField}
|
|
39
|
+
placeholder="Campo"
|
|
40
|
+
searchPlaceholder="Buscar campo…"
|
|
41
|
+
emptyMessage="Nenhum campo"
|
|
42
|
+
aria-label="Campo"
|
|
43
|
+
/>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Gotchas
|
|
47
|
+
|
|
48
|
+
- O trigger é um `<button role="combobox">` e espelha o `SelectTrigger` (mesma altura/borda/foco). Para parear com Selects irmãos num form, passe o mesmo `className` que você passaria ao `SelectTrigger`.
|
|
49
|
+
- A seleção **não** depende do argumento do `onSelect` do cmdk (que vem normalizado/lowercased) — o componente fecha via closure sobre `option.value`. Por isso `value`s com maiúsculas/acentos/espaços funcionam.
|
|
50
|
+
- O dropdown nasce com a **largura do trigger** (`--radix-popover-trigger-width`). Para largura própria, use `contentClassName="w-[...]"`.
|
|
51
|
+
- `label`s devem ser únicos (o cmdk indexa por `value`/label do item). Se houver rótulos repetidos, diferencie via `keywords`.
|