@iclips/ui 3.0.0 → 3.1.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/DESIGN.md +246 -0
- package/README.md +4 -0
- package/dist/components/ui/primitives/badge.cjs +1 -1
- package/dist/components/ui/primitives/badge.cjs.map +1 -1
- package/dist/components/ui/primitives/badge.js +16 -13
- package/dist/components/ui/primitives/badge.js.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/docs/README.md +44 -0
- package/docs/acoes.md +103 -0
- package/docs/escolha.md +171 -0
- package/docs/formularios.md +137 -0
- package/docs/mensagens.md +114 -0
- package/docs/overlays.md +192 -0
- package/docs/status.md +71 -0
- package/package.json +3 -1
package/dist/index.d.cts
CHANGED
|
@@ -171,7 +171,7 @@ export declare interface AdvancedDataTableProps<T extends object> {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
export declare const Alert: React_2.ForwardRefExoticComponent<Omit<React_2.ClassAttributes<HTMLDivElement> & React_2.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
|
|
174
|
-
variant?: "default" | "destructive" | "success" | "
|
|
174
|
+
variant?: "default" | "destructive" | "success" | "warning" | "info" | null | undefined;
|
|
175
175
|
} & ClassProp) | undefined) => string>, "ref"> & React_2.RefAttributes<HTMLDivElement>>;
|
|
176
176
|
|
|
177
177
|
export declare const AlertDescription: React_2.ForwardRefExoticComponent<Omit<React_2.DetailedHTMLProps<React_2.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React_2.RefAttributes<HTMLDivElement>>;
|
|
@@ -234,7 +234,7 @@ export declare const AvatarFallback: React_2.ForwardRefExoticComponent<PropsAvat
|
|
|
234
234
|
export declare const AvatarImage: React_2.ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarImageProps & React_2.RefAttributes<HTMLImageElement>, "ref"> & React_2.RefAttributes<HTMLImageElement>>;
|
|
235
235
|
|
|
236
236
|
export declare const Badge: React_2.ForwardRefExoticComponent<Omit<React_2.DetailedHTMLProps<React_2.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & VariantProps<(props?: ({
|
|
237
|
-
variant?: "default" | "destructive" | "outline" | "secondary" | "muted" | null | undefined;
|
|
237
|
+
variant?: "default" | "destructive" | "outline" | "secondary" | "success" | "warning" | "info" | "muted" | null | undefined;
|
|
238
238
|
size?: "default" | "xs" | null | undefined;
|
|
239
239
|
dashed?: boolean | null | undefined;
|
|
240
240
|
} & ClassProp) | undefined) => string> & {
|
|
@@ -244,7 +244,7 @@ export declare const Badge: React_2.ForwardRefExoticComponent<Omit<React_2.Detai
|
|
|
244
244
|
} & React_2.RefAttributes<HTMLSpanElement>>;
|
|
245
245
|
|
|
246
246
|
export declare const badgeVariants: (props?: ({
|
|
247
|
-
variant?: "default" | "destructive" | "outline" | "secondary" | "muted" | null | undefined;
|
|
247
|
+
variant?: "default" | "destructive" | "outline" | "secondary" | "success" | "warning" | "info" | "muted" | null | undefined;
|
|
248
248
|
size?: "default" | "xs" | null | undefined;
|
|
249
249
|
dashed?: boolean | null | undefined;
|
|
250
250
|
} & ClassProp) | undefined) => string;
|
package/dist/index.d.ts
CHANGED
|
@@ -171,7 +171,7 @@ export declare interface AdvancedDataTableProps<T extends object> {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
export declare const Alert: React_2.ForwardRefExoticComponent<Omit<React_2.ClassAttributes<HTMLDivElement> & React_2.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
|
|
174
|
-
variant?: "default" | "destructive" | "success" | "
|
|
174
|
+
variant?: "default" | "destructive" | "success" | "warning" | "info" | null | undefined;
|
|
175
175
|
} & ClassProp) | undefined) => string>, "ref"> & React_2.RefAttributes<HTMLDivElement>>;
|
|
176
176
|
|
|
177
177
|
export declare const AlertDescription: React_2.ForwardRefExoticComponent<Omit<React_2.DetailedHTMLProps<React_2.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React_2.RefAttributes<HTMLDivElement>>;
|
|
@@ -234,7 +234,7 @@ export declare const AvatarFallback: React_2.ForwardRefExoticComponent<PropsAvat
|
|
|
234
234
|
export declare const AvatarImage: React_2.ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarImageProps & React_2.RefAttributes<HTMLImageElement>, "ref"> & React_2.RefAttributes<HTMLImageElement>>;
|
|
235
235
|
|
|
236
236
|
export declare const Badge: React_2.ForwardRefExoticComponent<Omit<React_2.DetailedHTMLProps<React_2.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & VariantProps<(props?: ({
|
|
237
|
-
variant?: "default" | "destructive" | "outline" | "secondary" | "muted" | null | undefined;
|
|
237
|
+
variant?: "default" | "destructive" | "outline" | "secondary" | "success" | "warning" | "info" | "muted" | null | undefined;
|
|
238
238
|
size?: "default" | "xs" | null | undefined;
|
|
239
239
|
dashed?: boolean | null | undefined;
|
|
240
240
|
} & ClassProp) | undefined) => string> & {
|
|
@@ -244,7 +244,7 @@ export declare const Badge: React_2.ForwardRefExoticComponent<Omit<React_2.Detai
|
|
|
244
244
|
} & React_2.RefAttributes<HTMLSpanElement>>;
|
|
245
245
|
|
|
246
246
|
export declare const badgeVariants: (props?: ({
|
|
247
|
-
variant?: "default" | "destructive" | "outline" | "secondary" | "muted" | null | undefined;
|
|
247
|
+
variant?: "default" | "destructive" | "outline" | "secondary" | "success" | "warning" | "info" | "muted" | null | undefined;
|
|
248
248
|
size?: "default" | "xs" | null | undefined;
|
|
249
249
|
dashed?: boolean | null | undefined;
|
|
250
250
|
} & ClassProp) | undefined) => string;
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @iclips/ui — como usar
|
|
2
|
+
|
|
3
|
+
Leia isto antes de montar uma tela com a `@iclips/ui`. Os guias abaixo dizem **qual componente usar em cada situação** e trazem código pronto para copiar. Eles vêm no pacote, então descrevem exatamente a versão instalada.
|
|
4
|
+
|
|
5
|
+
Todo import vem do barrel: `import { Button } from "@iclips/ui"`. Os componentes de edição rica (`DocumentEditor`, `CommentSystem`, `EditorialCalendar`, `MentionList`) vêm de `@iclips/ui/editor`. Importe `@iclips/ui/styles.css` uma vez na raiz do app.
|
|
6
|
+
|
|
7
|
+
## Guias
|
|
8
|
+
|
|
9
|
+
- [Ações e navegação](acoes.md) — botão, link ou ícone; hierarquia de variantes.
|
|
10
|
+
- [Escolha em lista](escolha.md) — Select, Combobox, RadioGroup, Checkbox, Switch, ToggleGroup.
|
|
11
|
+
- [Formulários](formularios.md) — anatomia do campo, validação, layout, react-hook-form.
|
|
12
|
+
- [Mensagens ao usuário](mensagens.md) — erro de campo, Alert, Toast, AlertDialog.
|
|
13
|
+
- [Overlays](overlays.md) — Tooltip, HoverCard, Popover, Dialog, AlertDialog, Sheet, Drawer.
|
|
14
|
+
- [Status](status.md) — badge de status, valor positivo/negativo.
|
|
15
|
+
- [Linguagem visual](../DESIGN.md) — as quatro camadas de cor, tipografia, raio, botões e superfícies.
|
|
16
|
+
|
|
17
|
+
## Troque isto por aquilo
|
|
18
|
+
|
|
19
|
+
As classes cruas da paleta do Tailwind **não geram CSS** na `@iclips/ui` 3.x: o elemento fica sem cor, em silêncio. Use o token semântico ou o componente.
|
|
20
|
+
|
|
21
|
+
| Em vez de | Use |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `text-gray-500` | `text-muted-foreground` |
|
|
24
|
+
| `text-gray-600` | `text-muted-foreground` |
|
|
25
|
+
| `text-gray-900` | `text-foreground` |
|
|
26
|
+
| `border-gray-200` | `border-border` |
|
|
27
|
+
| `border-gray-300` | `border-input` |
|
|
28
|
+
| `text-red-500` | `text-destructive-emphasis` |
|
|
29
|
+
| `text-green-500` | `text-success-emphasis` |
|
|
30
|
+
| `bg-green-50` | `Badge` ou `Alert` com `variant="success"` — ver [Status](status.md) |
|
|
31
|
+
| `bg-orange-100` | `Badge` ou `Alert` com `variant="warning"` — ver [Status](status.md) |
|
|
32
|
+
| `<button>` | `Button` — ver [Ações](acoes.md) |
|
|
33
|
+
| `<input>` | `Input` (ou `NumberInput`, `CurrencyInput`, `DatePicker`, `PasswordInput`) |
|
|
34
|
+
| `<select>` | `Select` (ou `Combobox` para lista longa) — ver [Escolha](escolha.md) |
|
|
35
|
+
|
|
36
|
+
Para achar cor crua no app: `node node_modules/@iclips/ui/scripts/check-colors.mjs src`.
|
|
37
|
+
|
|
38
|
+
## Quando não houver guia
|
|
39
|
+
|
|
40
|
+
1. Procure o componente no barrel antes de montar um na mão. Elemento HTML cru (`<button>`, `<input>`, `<select>`, `<div onClick>`) quase sempre tem equivalente.
|
|
41
|
+
2. Cor sempre por token semântico: `bg-background`, `bg-card`, `bg-muted`, `text-foreground`, `text-muted-foreground`, `border-border`, `bg-primary`. Nunca cor crua da paleta.
|
|
42
|
+
3. Espaçamento e tamanho pela escala (`gap-4`, `p-6`, `max-w-sm`). Nunca valor arbitrário (`p-[13px]`) nem `style={{}}` com valor literal.
|
|
43
|
+
4. Ícones de `lucide-react`.
|
|
44
|
+
5. Estado (erro, sucesso, aviso, informação) segue a receita suave: `bg-<status>/10 text-<status>-emphasis border-<status>/20` — de preferência pelo componente que já a aplica (`Badge`, `Alert`, `Button`).
|
package/docs/acoes.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Botão, link ou ícone
|
|
2
|
+
|
|
3
|
+
A regra de ouro: **link leva a algum lugar** (muda a URL), **botão faz alguma coisa** (dispara uma ação na página). Escolher errado quebra abrir-em-nova-aba, o botão voltar e o leitor de tela.
|
|
4
|
+
|
|
5
|
+
## Elemento certo
|
|
6
|
+
|
|
7
|
+
| Elemento | Escolha quando | Evite quando |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `<a>` / `<Link>` | o clique navega para outra rota, âncora ou recurso externo. Suporta Ctrl+clique, botão do meio, "copiar link". | o clique só muda estado na tela (abrir modal, submeter, alternar) → `<button>`. |
|
|
10
|
+
| `Button` | dispara uma ação: salvar, abrir overlay, adicionar item, filtrar. É o padrão para quase tudo que não é navegação. | o destino é uma URL → renderize um link (use `asChild`). |
|
|
11
|
+
| `Button variant="link"` | uma ação secundária que precisa parecer texto ("Esqueci a senha", "ver todos") dentro de um parágrafo ou rodapé. | é a ação principal do bloco → `default`/`secondary`. É navegação de verdade → `<a>` estilizado. |
|
|
12
|
+
| `Button size="icon"` | ação repetida numa linha/toolbar onde o texto polui (excluir linha, favoritar). Sempre com `aria-label`. | a ação é rara ou ambígua — aí o texto ajuda. Ou é a ação primária de um formulário. |
|
|
13
|
+
| `SocialButton` | login/conexão com provedor (Google, Facebook…) — já traz ícone e rótulo do provedor. | qualquer outra ação. |
|
|
14
|
+
|
|
15
|
+
## Hierarquia de variantes
|
|
16
|
+
|
|
17
|
+
Uma ação primária por bloco. O resto desce na escala.
|
|
18
|
+
|
|
19
|
+
### Ordem de ênfase
|
|
20
|
+
|
|
21
|
+
Rótulo em peso 600 (semibold) — no sistema, 600 significa ação. Hover de `outline` e `ghost` é cinza; lavanda fica para selecionado.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { Button } from "@iclips/ui";
|
|
25
|
+
|
|
26
|
+
<Button>Primária — salvar, confirmar, criar</Button>
|
|
27
|
+
<Button variant="secondary">Secundária — ação alternativa</Button>
|
|
28
|
+
<Button variant="outline">Terciária — cancelar, voltar</Button>
|
|
29
|
+
<Button variant="ghost">Discreta — em toolbars, dentro de cards</Button>
|
|
30
|
+
<Button variant="link">Link — ação textual inline</Button>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Semânticas
|
|
34
|
+
|
|
35
|
+
`destructive` só para remoção/irreversível. `success` é raro — normalmente o `default` basta.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { Button } from "@iclips/ui";
|
|
39
|
+
import { Trash2 } from "lucide-react";
|
|
40
|
+
|
|
41
|
+
<Button variant="destructive"><Trash2 /> Excluir</Button>
|
|
42
|
+
<Button variant="success">Aprovar</Button>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Com ícone
|
|
46
|
+
|
|
47
|
+
Ícone à esquerda reforça o verbo; à direita sugere avanço/navegação.
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { Button } from "@iclips/ui";
|
|
51
|
+
import { ArrowRight, Download, Plus } from "lucide-react";
|
|
52
|
+
|
|
53
|
+
<Button><Plus /> Nova campanha</Button>
|
|
54
|
+
<Button variant="outline">Exportar <Download /></Button>
|
|
55
|
+
<Button variant="link">Ver relatório <ArrowRight /></Button>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Trilha rápida
|
|
59
|
+
|
|
60
|
+
1. **O clique muda a URL / abre outra página?** Sim: é um link. Renderize `<a>` (ou `<Link>` do router) — via `Button asChild` se quiser o visual de botão. Não: continue.
|
|
61
|
+
2. **É a ação principal deste bloco/formulário?** Sim: `Button variant="default"`. Não: continue.
|
|
62
|
+
3. **Remove ou desfaz algo de forma difícil de reverter?** Sim: `variant="destructive"` + confirmação (ver [Overlays](overlays.md)). Não: continue.
|
|
63
|
+
4. **Está numa linha de tabela / toolbar densa e o verbo é óbvio pelo ícone?** Sim: `size="icon"` + `aria-label`. Não: variant `secondary` / `outline` / `ghost` conforme a ênfase.
|
|
64
|
+
|
|
65
|
+
## Link com aparência de botão
|
|
66
|
+
|
|
67
|
+
Quando o destino é uma URL mas o design pede um botão, use `asChild` — o elemento vira `<a>`, o estilo continua.
|
|
68
|
+
|
|
69
|
+
### asChild
|
|
70
|
+
|
|
71
|
+
Renderiza `<a href>` com as classes do `Button`. Mantém Ctrl+clique e menu de contexto.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { Button } from "@iclips/ui";
|
|
75
|
+
import { ArrowRight } from "lucide-react";
|
|
76
|
+
|
|
77
|
+
<Button asChild>
|
|
78
|
+
<a href="/projetos/42">Abrir projeto</a>
|
|
79
|
+
</Button>
|
|
80
|
+
<Button asChild variant="outline">
|
|
81
|
+
<a href="https://iclips.com.br" target="_blank" rel="noreferrer">
|
|
82
|
+
Site da iClips <ArrowRight />
|
|
83
|
+
</a>
|
|
84
|
+
</Button>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Faça / evite
|
|
88
|
+
|
|
89
|
+
#### Faça
|
|
90
|
+
|
|
91
|
+
- Comece o rótulo com um verbo: "Salvar alterações", não "OK".
|
|
92
|
+
- Toda ação só-ícone leva `aria-label` com o mesmo verbo que um Tooltip mostraria.
|
|
93
|
+
- Use `isLoading` para travar o botão durante a requisição — ele já seta `aria-busy` e bloqueia o clique.
|
|
94
|
+
- Um `<a>` para navegação mesmo que estilizado como botão — o usuário espera Ctrl+clique.
|
|
95
|
+
|
|
96
|
+
#### Evite
|
|
97
|
+
|
|
98
|
+
- `onClick={() => navigate("/rota")}` num `<button>` — quebra abrir em nova aba e o histórico.
|
|
99
|
+
- Dois botões primários lado a lado — o olho não sabe qual é o caminho feliz.
|
|
100
|
+
- `<div onClick>` — sem foco, sem Enter/Espaço, invisível para leitor de tela.
|
|
101
|
+
- `variant="link"` para a ação principal de um formulário.
|
|
102
|
+
|
|
103
|
+
**Acessibilidade:** o `Button` já traz anel de foco visível, `active:scale` e trata `disabled`. Um link precisa de `href` real para ser focável e anunciado como link — `role="button"` num `<a>` ou vice-versa confunde a tecnologia assistiva. Ícone sem texto *sempre* com `aria-label`.
|
package/docs/escolha.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Como o usuário escolhe de uma lista
|
|
2
|
+
|
|
3
|
+
Duas perguntas resolvem quase tudo: **uma opção ou várias?** e **quantas opções existem** — porque abaixo de ~7 vale mostrar todas, e acima disso é preciso busca.
|
|
4
|
+
|
|
5
|
+
## Uma única escolha
|
|
6
|
+
|
|
7
|
+
| Componente | Escolha quando | Evite quando |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| RadioGroup | 2 a 5 opções que valem ser vistas todas de uma vez, e a escolha é importante (plano, forma de pagamento). | mais de ~6 opções, ou espaço vertical apertado → `Select`. |
|
|
10
|
+
| Select | 6 a ~25 opções conhecidas e curtas (estado, categoria, status). Economiza espaço. | a lista é longa ou o usuário sabe o nome do que quer → `Combobox`. |
|
|
11
|
+
| Combobox | lista longa (>25) ou dinâmica: cidade, cliente, produto. Filtra enquanto digita. | são poucas opções fixas — o campo de busca vira fricção → `Select` ou Radio. |
|
|
12
|
+
| ToggleGroup (single) | 2 a 4 opções mutuamente exclusivas que mudam a visão na hora: alinhamento, período (dia/semana/mês), modo de exibição. | é um dado do formulário a ser submetido, não uma alternância de UI → Radio. |
|
|
13
|
+
|
|
14
|
+
## Várias escolhas (ou liga/desliga)
|
|
15
|
+
|
|
16
|
+
| Componente | Escolha quando | Evite quando |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| Checkbox (grupo) | 2 a ~8 opções independentes visíveis de uma vez: permissões, dias da semana, filtros marcáveis. | a lista é longa → `MultiSelect`. É uma única opção booleana isolada → Checkbox sozinho ou `Switch`. |
|
|
19
|
+
| MultiSelect | escolher vários de uma lista longa: responsáveis, tags, canais. Mostra chips do que foi escolhido + busca. | poucas opções fixas → grupo de Checkbox mostra tudo sem cliques extras. |
|
|
20
|
+
| ToggleGroup (multiple) | conjunto pequeno de formatações/filtros que ligam e desligam juntos: negrito/itálico, tipos de mídia. | opções com rótulos longos, ou muitas → Checkbox. |
|
|
21
|
+
| Checkbox (isolado) | uma afirmação que o usuário aceita ou não: "Aceito os termos", "Lembrar de mim". Faz parte de um envio. | liga um comportamento imediatamente, sem submit → `Switch`. |
|
|
22
|
+
| Switch | liga/desliga uma configuração com efeito imediato: notificações, modo escuro, tema. | o efeito só acontece ao salvar o formulário → Checkbox. |
|
|
23
|
+
|
|
24
|
+
## Trilha rápida
|
|
25
|
+
|
|
26
|
+
1. **O usuário pode escolher mais de uma opção?** Sim: poucas e fixas → grupo de `Checkbox`; muitas/dinâmicas → `MultiSelect`. Não: continue.
|
|
27
|
+
2. **É um liga/desliga com efeito imediato (sem salvar)?** Sim: `Switch`. Não: continue.
|
|
28
|
+
3. **A escolha muda a visão da tela na hora (período, layout)?** Sim: `ToggleGroup type="single"`. Não: continue.
|
|
29
|
+
4. **Quantas opções?** Até 5 e importantes → `RadioGroup` · 6 a 25 → `Select` · mais que isso ou dinâmica → `Combobox`.
|
|
30
|
+
|
|
31
|
+
## Exemplos
|
|
32
|
+
|
|
33
|
+
Os exemplos usam esta lista:
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
const canais = [
|
|
37
|
+
{ value: "ig", label: "Instagram" },
|
|
38
|
+
{ value: "fb", label: "Facebook" },
|
|
39
|
+
{ value: "li", label: "LinkedIn" },
|
|
40
|
+
{ value: "tt", label: "TikTok" },
|
|
41
|
+
{ value: "yt", label: "YouTube" },
|
|
42
|
+
];
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### RadioGroup — poucas, todas visíveis
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { Label, RadioGroup, RadioGroupItem } from "@iclips/ui";
|
|
49
|
+
|
|
50
|
+
<RadioGroup defaultValue="anual" className="gap-2">
|
|
51
|
+
{["mensal", "anual", "vitalício"].map((v) => (
|
|
52
|
+
<div key={v} className="flex items-center gap-2">
|
|
53
|
+
<RadioGroupItem value={v} id={`plano-${v}`} />
|
|
54
|
+
<Label htmlFor={`plano-${v}`} className="capitalize">{v}</Label>
|
|
55
|
+
</div>
|
|
56
|
+
))}
|
|
57
|
+
</RadioGroup>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Select — lista média conhecida
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@iclips/ui";
|
|
64
|
+
|
|
65
|
+
<Select>
|
|
66
|
+
<SelectTrigger className="w-56" aria-label="Canal"><SelectValue placeholder="Selecione o canal" /></SelectTrigger>
|
|
67
|
+
<SelectContent>
|
|
68
|
+
{canais.map((c) => (
|
|
69
|
+
<SelectItem key={c.value} value={c.value}>{c.label}</SelectItem>
|
|
70
|
+
))}
|
|
71
|
+
</SelectContent>
|
|
72
|
+
</Select>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Combobox — busca em lista longa
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { Combobox } from "@iclips/ui";
|
|
79
|
+
|
|
80
|
+
const [canal, setCanal] = React.useState("");
|
|
81
|
+
|
|
82
|
+
<Combobox options={canais} value={canal} onValueChange={setCanal} placeholder="Buscar canal" aria-label="Canal" />
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### MultiSelect — vários, com chips
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { MultiSelect } from "@iclips/ui";
|
|
89
|
+
|
|
90
|
+
const [selecionados, setSelecionados] = React.useState<string[]>(["ig"]);
|
|
91
|
+
|
|
92
|
+
<MultiSelect
|
|
93
|
+
options={canais}
|
|
94
|
+
value={selecionados}
|
|
95
|
+
onValueChange={setSelecionados}
|
|
96
|
+
placeholder="Canais da campanha"
|
|
97
|
+
aria-label="Canais da campanha"
|
|
98
|
+
/>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Checkbox (grupo) — independentes, visíveis
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { Checkbox } from "@iclips/ui";
|
|
105
|
+
|
|
106
|
+
<div className="space-y-2">
|
|
107
|
+
{["Ler", "Escrever", "Excluir"].map((p) => (
|
|
108
|
+
<label key={p} className="flex items-center gap-2 text-sm">
|
|
109
|
+
<Checkbox defaultChecked={p === "Ler"} /> {p}
|
|
110
|
+
</label>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Switch — efeito imediato
|
|
116
|
+
|
|
117
|
+
Muda algo agora. Checkbox seria para um valor a ser salvo.
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { Switch } from "@iclips/ui";
|
|
121
|
+
|
|
122
|
+
<label className="flex items-center gap-2 text-sm">
|
|
123
|
+
<Switch defaultChecked /> Notificações por e-mail
|
|
124
|
+
</label>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### ToggleGroup — troca de visão
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { ToggleGroup, ToggleGroupItem } from "@iclips/ui";
|
|
131
|
+
|
|
132
|
+
<ToggleGroup type="single" defaultValue="semana">
|
|
133
|
+
<ToggleGroupItem value="dia">Dia</ToggleGroupItem>
|
|
134
|
+
<ToggleGroupItem value="semana">Semana</ToggleGroupItem>
|
|
135
|
+
<ToggleGroupItem value="mes">Mês</ToggleGroupItem>
|
|
136
|
+
</ToggleGroup>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Estados do item: selecionado > focado > sob o cursor
|
|
140
|
+
|
|
141
|
+
Os três podem coexistir no mesmo item e cada um usa um sinal diferente, para continuarem legíveis empilhados. `Combobox` e `MultiSelect` já seguem essa regra; siga-a em listas próprias.
|
|
142
|
+
|
|
143
|
+
| Estado | Como se pinta | Por quê |
|
|
144
|
+
| --- | --- | --- |
|
|
145
|
+
| Selecionado | `bg-secondary text-secondary-foreground font-medium` | matiz + tinta + peso: três sinais, sobrevive ao hover por cima. |
|
|
146
|
+
| Focado por teclado | `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset` | anel, não fundo — sobrevive sobre qualquer fundo. |
|
|
147
|
+
| Sob o cursor | `hover:bg-accent` | cinza, transitório, sem peso. |
|
|
148
|
+
|
|
149
|
+
Hover e seleção são ambos `bg-*`: quem vence é a ordem do CSS gerado, não a do `className`. Guarde o hover pelo estado (`not-aria-selected:hover:bg-accent`) ou inclua `hover:bg-secondary` na classe de selecionado, para o `cn` descartar o hover conflitante.
|
|
150
|
+
|
|
151
|
+
### Ilustração estática — os três estados na mesma lista
|
|
152
|
+
|
|
153
|
+
Numa mesma lista, o item selecionado fica lavanda com peso maior, o focado ganha só o anel e o que está sob o cursor fica cinza — nunca lavanda, que significa selecionado.
|
|
154
|
+
|
|
155
|
+
## Faça / evite
|
|
156
|
+
|
|
157
|
+
#### Faça
|
|
158
|
+
|
|
159
|
+
- Todo controle com `<Label htmlFor>` — clicar no texto ativa o controle.
|
|
160
|
+
- `Combobox` e `MultiSelect` quando a lista vem da API ou passa de ~25 itens.
|
|
161
|
+
- `RadioGroup` com um default sensato já selecionado (evita estado "nada escolhido").
|
|
162
|
+
- `Switch` só para o que aplica na hora; se tem botão "Salvar", é `Checkbox`.
|
|
163
|
+
|
|
164
|
+
#### Evite
|
|
165
|
+
|
|
166
|
+
- `Select` com 3 opções — `RadioGroup` mostra as 3 sem um clique a mais.
|
|
167
|
+
- Grupo de `Checkbox` com 40 itens — vira uma parede; use `MultiSelect`.
|
|
168
|
+
- Radio e Checkbox trocados: Radio = uma; Checkbox = zero ou mais.
|
|
169
|
+
- `Combobox` para sim/não.
|
|
170
|
+
|
|
171
|
+
**Acessibilidade:** `Select`, `Combobox`, `RadioGroup`, `Checkbox` e `Switch` já trazem navegação por seta, Home/End, digitar-para-focar e os papéis ARIA. O `MultiSelect` é custom: ele já trata teclado, mas confirme o `aria-label` quando não houver rótulo visível.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Montando um formulário
|
|
2
|
+
|
|
3
|
+
Um formulário bom é **previsível**: label sempre acima do campo, erro sempre logo abaixo, ação primária sempre no mesmo canto. As decisões abaixo são o que muda de tela pra tela.
|
|
4
|
+
|
|
5
|
+
## Anatomia de um campo
|
|
6
|
+
|
|
7
|
+
A ordem é sempre esta, de cima para baixo:
|
|
8
|
+
|
|
9
|
+
1. **Label** acima, sempre visível (nunca só placeholder).
|
|
10
|
+
2. **Campo**.
|
|
11
|
+
3. **Ajuda** (opcional), ligada por `aria-describedby`.
|
|
12
|
+
4. **Erro** no lugar da ajuda quando houver, com `aria-invalid` no campo.
|
|
13
|
+
|
|
14
|
+
### Campo completo
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { Input, Label } from "@iclips/ui";
|
|
18
|
+
|
|
19
|
+
<div className="grid w-full max-w-xs gap-1.5">
|
|
20
|
+
<Label htmlFor="site">
|
|
21
|
+
Site <span className="text-muted-foreground">(opcional)</span>
|
|
22
|
+
</Label>
|
|
23
|
+
<Input id="site" placeholder="https://" aria-describedby="site-ajuda" />
|
|
24
|
+
<p id="site-ajuda" className="text-sm text-muted-foreground">
|
|
25
|
+
Aparece no rodapé das propostas.
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Com erro
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { Input, Label } from "@iclips/ui";
|
|
34
|
+
|
|
35
|
+
<div className="grid w-full max-w-xs gap-1.5">
|
|
36
|
+
<Label htmlFor="email">E-mail</Label>
|
|
37
|
+
<Input id="email" defaultValue="ana@" aria-invalid aria-describedby="email-erro" />
|
|
38
|
+
<p id="email-erro" className="text-sm text-destructive-emphasis">Informe um e-mail válido.</p>
|
|
39
|
+
</div>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Obrigatório ou opcional?
|
|
43
|
+
|
|
44
|
+
Marque a minoria. Nunca os dois.
|
|
45
|
+
|
|
46
|
+
| Caso | Escolha | Evite |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| maioria obrigatória | marque só os opcionais com "(opcional)" em texto ao lado do label. | asterisco em quase todos os campos — vira ruído e ninguém lê. |
|
|
49
|
+
| maioria opcional | marque os obrigatórios com "*" (ou "obrigatório") e explique o "*" uma vez no topo. | marcar "(opcional)" em 15 campos. |
|
|
50
|
+
|
|
51
|
+
## Quando é validação inline vs no envio
|
|
52
|
+
|
|
53
|
+
1. **O erro só pode ser sabido pelo servidor (e-mail já existe, saldo insuficiente)?** Sim: valide no envio (ou no blur com debounce, se a API aguentar). Mostre no campo. Não: continue.
|
|
54
|
+
2. **É formato/obrigatoriedade que dá pra checar no cliente?** Sim: valide no blur (`mode: "onTouched"`) — erra cedo, sem incomodar quem ainda está digitando. Não: continue.
|
|
55
|
+
3. **O formulário é curto (1–3 campos, ex: login)?** Sim: pode validar só no envio — o usuário vê tudo de uma vez. Não: blur + um resumo no topo (`Alert`) ao tentar enviar com erros. Foque o primeiro campo inválido.
|
|
56
|
+
|
|
57
|
+
## Layout
|
|
58
|
+
|
|
59
|
+
| Layout | Escolha quando | Evite quando |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| uma coluna | o padrão. Fluxo de leitura vertical, funciona em qualquer largura, não confunde a ordem de tab. | quase nunca — só saia disso com motivo. |
|
|
62
|
+
| duas colunas | pares que andam juntos e são curtos: cidade/UF, início/fim, DDD/telefone. Agrupe visualmente. | campos longos ou não relacionados lado a lado — a leitura zigue-zagueia. |
|
|
63
|
+
| seções (fieldset) | formulário longo com blocos temáticos: Dados da conta / Endereço / Preferências. Título por bloco. | wizard de várias telas quando tudo cabe numa página com rolagem. |
|
|
64
|
+
|
|
65
|
+
## Botões de ação
|
|
66
|
+
|
|
67
|
+
#### Faça
|
|
68
|
+
|
|
69
|
+
- Ação primária à direita (ou sozinha, largura total no mobile). Uma só.
|
|
70
|
+
- Rótulo com o verbo da ação: "Criar projeto", "Salvar alterações" — não "Enviar" / "OK".
|
|
71
|
+
- Cancelar/Voltar como variante `outline` ou `ghost`, à esquerda da primária.
|
|
72
|
+
- Durante o envio: `isLoading` no botão (trava e mostra spinner) — não um overlay na tela toda.
|
|
73
|
+
|
|
74
|
+
#### Evite
|
|
75
|
+
|
|
76
|
+
- Dois botões com o mesmo peso visual (dois primários).
|
|
77
|
+
- "Limpar" ao lado de "Salvar" — o acidente é caro; se precisar, deixe longe e peça confirmação.
|
|
78
|
+
- Desabilitar o botão de envio enquanto há erro — o usuário não descobre o que falta. Deixe habilitado e valide no clique.
|
|
79
|
+
|
|
80
|
+
## react-hook-form + os componentes
|
|
81
|
+
|
|
82
|
+
Use `FormField` para amarrar tudo pelos ids certos automaticamente.
|
|
83
|
+
|
|
84
|
+
### FormField
|
|
85
|
+
|
|
86
|
+
`FormControl` injeta `id`/`aria-invalid`/`aria-describedby`; `FormMessage` some quando não há erro.
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { useForm } from "react-hook-form";
|
|
90
|
+
import {
|
|
91
|
+
Button, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Input, Textarea,
|
|
92
|
+
} from "@iclips/ui";
|
|
93
|
+
|
|
94
|
+
type Campos = { nome: string; obs: string };
|
|
95
|
+
|
|
96
|
+
function NovoProjeto() {
|
|
97
|
+
const form = useForm<Campos>({ mode: "onTouched", defaultValues: { nome: "", obs: "" } });
|
|
98
|
+
return (
|
|
99
|
+
<Form {...form}>
|
|
100
|
+
<form onSubmit={form.handleSubmit(salvar)} className="grid w-full max-w-sm gap-4" noValidate>
|
|
101
|
+
<FormField
|
|
102
|
+
control={form.control}
|
|
103
|
+
name="nome"
|
|
104
|
+
rules={{ required: "Informe o nome." }}
|
|
105
|
+
render={({ field }) => (
|
|
106
|
+
<FormItem>
|
|
107
|
+
<FormLabel>Nome do projeto</FormLabel>
|
|
108
|
+
<FormControl><Input placeholder="Rebranding Aurora" {...field} /></FormControl>
|
|
109
|
+
<FormMessage />
|
|
110
|
+
</FormItem>
|
|
111
|
+
)}
|
|
112
|
+
/>
|
|
113
|
+
<FormField
|
|
114
|
+
control={form.control}
|
|
115
|
+
name="obs"
|
|
116
|
+
render={({ field }) => (
|
|
117
|
+
<FormItem>
|
|
118
|
+
<FormLabel>Observações <span className="text-muted-foreground">(opcional)</span></FormLabel>
|
|
119
|
+
<FormControl><Textarea rows={3} {...field} /></FormControl>
|
|
120
|
+
<FormDescription>Visível só para a equipe.</FormDescription>
|
|
121
|
+
<FormMessage />
|
|
122
|
+
</FormItem>
|
|
123
|
+
)}
|
|
124
|
+
/>
|
|
125
|
+
<div className="flex justify-end gap-2">
|
|
126
|
+
<Button type="button" variant="outline">Cancelar</Button>
|
|
127
|
+
<Button type="submit">Criar projeto</Button>
|
|
128
|
+
</div>
|
|
129
|
+
</form>
|
|
130
|
+
</Form>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Qual input para cada dado?** Está em [Escolha em lista](escolha.md) (Select vs Combobox vs Radio…). Para valores: `NumberInput`/`CurrencyInput` para números, `DatePicker`/`DateRangePicker` para datas, `PasswordInput` para senha, `InputOTP` para código, `InputGroup` quando o campo tem prefixo/sufixo (R$, .com, botão).
|
|
136
|
+
|
|
137
|
+
**Acessibilidade:** todo campo com `Label htmlFor` apontando pro `id` — clicar no texto foca o campo e o leitor de tela anuncia o rótulo. `aria-describedby` liga ajuda e erro. Ao enviar com erros, mova o foco para o resumo ou para o primeiro campo inválido.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Onde a mensagem aparece
|
|
2
|
+
|
|
3
|
+
Três eixos decidem: **é resposta a uma ação** ou uma condição persistente? **Precisa de confirmação** ou só informa? **É sobre um campo** ou sobre a tela toda?
|
|
4
|
+
|
|
5
|
+
## Tabela de decisão
|
|
6
|
+
|
|
7
|
+
| Mensagem | Escolha quando | Evite quando |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| Erro de campo | a mensagem é sobre um input específico: formato inválido, obrigatório, indisponível. Fica abaixo do campo, com `aria-invalid`. | o problema é do formulário inteiro (ex: "falha de conexão ao salvar") → `Alert` no topo do form. |
|
|
10
|
+
| Alert (inline) | uma condição que permanece enquanto o usuário olha a tela: aviso de plano expirando, banner de modo somente-leitura, resumo de erros de validação. | é reação efêmera a um clique que acabou de acontecer → `Toast`. |
|
|
11
|
+
| Toast | confirmação passageira de uma ação: "Salvo", "E-mail enviado", "Falha ao publicar". Some sozinho. Pode ter ação "Desfazer". | a informação é importante e não pode passar batida, ou exige decisão → `Alert` ou `AlertDialog`. |
|
|
12
|
+
| AlertDialog | a ação é destrutiva e você precisa que o usuário pare e confirme antes. Trava a tela. (Ver [Overlays](overlays.md).) | a ação é desfazível → execute e ofereça "Desfazer" num `Toast`. |
|
|
13
|
+
|
|
14
|
+
## Trilha rápida
|
|
15
|
+
|
|
16
|
+
1. **A mensagem é sobre um campo específico do formulário?** Sim: erro de campo abaixo do input + `aria-invalid` no input. Não: continue.
|
|
17
|
+
2. **Você precisa bloquear a tela até o usuário decidir?** Sim: `AlertDialog`. Não: continue.
|
|
18
|
+
3. **É reação imediata a um clique e pode desaparecer em segundos?** Sim: Toast (`toast.success` / `toast.error` / …). Se a ação for reversível, inclua "Desfazer". Não: continue.
|
|
19
|
+
4. **A condição continua verdadeira enquanto a tela estiver aberta?** Sim: `Alert` inline, ancorado onde a condição importa. Não: provavelmente não precisa de mensagem — reavalie.
|
|
20
|
+
|
|
21
|
+
## Exemplos
|
|
22
|
+
|
|
23
|
+
### Toast — confirmação efêmera
|
|
24
|
+
|
|
25
|
+
Dispare no callback da ação. Prefira `toast.success`/`toast.error` a texto neutro. Monte um único `<Toaster />` na raiz do app.
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { Button, toast } from "@iclips/ui";
|
|
29
|
+
|
|
30
|
+
<Button variant="outline" onClick={() => toast.success("Campanha salva")}>Salvar</Button>
|
|
31
|
+
<Button
|
|
32
|
+
variant="outline"
|
|
33
|
+
onClick={() =>
|
|
34
|
+
toast("Item removido", {
|
|
35
|
+
action: { label: "Desfazer", onClick: () => toast("Restaurado") },
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
>
|
|
39
|
+
Remover (com desfazer)
|
|
40
|
+
</Button>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Alert — condição persistente
|
|
44
|
+
|
|
45
|
+
Fica na tela. Título curto + o que fazer a respeito.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { Alert, AlertDescription, AlertTitle } from "@iclips/ui";
|
|
49
|
+
import { TriangleAlert } from "lucide-react";
|
|
50
|
+
|
|
51
|
+
<Alert variant="warning" className="max-w-sm">
|
|
52
|
+
<TriangleAlert />
|
|
53
|
+
<AlertTitle>Plano expira em 3 dias</AlertTitle>
|
|
54
|
+
<AlertDescription>Renove para não perder o acesso às automações.</AlertDescription>
|
|
55
|
+
</Alert>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Erro de campo
|
|
59
|
+
|
|
60
|
+
A mensagem vive com o input, não no topo da tela. Em formulário com react-hook-form, `FormMessage` faz isso sozinho (ver [Formulários](formularios.md)).
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { Input, Label } from "@iclips/ui";
|
|
64
|
+
|
|
65
|
+
<div className="grid w-full max-w-xs gap-1.5">
|
|
66
|
+
<Label htmlFor="email">E-mail</Label>
|
|
67
|
+
<Input id="email" defaultValue="ana@" aria-invalid aria-describedby="email-erro" />
|
|
68
|
+
<p id="email-erro" className="text-sm text-destructive-emphasis">
|
|
69
|
+
Informe um e-mail válido.
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Alert — resumo de validação
|
|
75
|
+
|
|
76
|
+
Erros do formulário inteiro sobem para um `Alert` no topo, com foco.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { Alert, AlertDescription, AlertTitle } from "@iclips/ui";
|
|
80
|
+
import { Info } from "lucide-react";
|
|
81
|
+
|
|
82
|
+
<Alert variant="destructive" className="max-w-sm" role="alert">
|
|
83
|
+
<Info />
|
|
84
|
+
<AlertTitle>Não foi possível salvar</AlertTitle>
|
|
85
|
+
<AlertDescription>Revise os 2 campos destacados abaixo.</AlertDescription>
|
|
86
|
+
</Alert>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Faça / evite
|
|
90
|
+
|
|
91
|
+
#### Faça
|
|
92
|
+
|
|
93
|
+
- Monte um único `<Toaster />` na raiz do app. Chame `toast(...)` de qualquer lugar.
|
|
94
|
+
- Toast de erro: diga o que falhou e o próximo passo ("Falha ao publicar — verifique a conexão").
|
|
95
|
+
- Ação reversível → execute já e ofereça "Desfazer" no toast, em vez de um `AlertDialog` antes.
|
|
96
|
+
- Toast de erro fica até o usuário fechar (padrão do `toast.error`); sucesso some em 4s, aviso em 6s, informação em 5s.
|
|
97
|
+
- Escuro = ação com prazo: `toast(msg, { action })` sem tipo vem em superfície invertida e dura 8s. Use só para Desfazer.
|
|
98
|
+
- Progresso: `toast.promise` — o toast de processando se transforma em sucesso ou erro, em vez de somar outro na pilha.
|
|
99
|
+
- Ação em lote gera um toast agregado ("12 jobs atualizados"), não doze.
|
|
100
|
+
- Erro de campo: ligue input e mensagem com `aria-describedby` + `aria-invalid`.
|
|
101
|
+
- Aviso: `text-warning-emphasis`, nunca `text-warning`, e sempre com ícone — amarelo sozinho não identifica um aviso.
|
|
102
|
+
|
|
103
|
+
#### Evite
|
|
104
|
+
|
|
105
|
+
- Toast para algo que o usuário precisa ler com atenção — ele some antes.
|
|
106
|
+
- Alert de sucesso permanente que nunca some e vira ruído visual.
|
|
107
|
+
- `AlertDialog` "Salvo com sucesso! [OK]" — é o caso clássico de Toast.
|
|
108
|
+
- Empilhar 5 toasts de uma vez; agrupe ou resuma.
|
|
109
|
+
|
|
110
|
+
**Toast:** `toast` (sonner) é a única API — `toast.success` / `toast.error` / `toast.promise`, `action`, `duration`, `toast.dismiss`. Monte um `<Toaster />` na raiz do app. Superfície neutra sempre, cor só no ícone; o único toast roxo é o de processando.
|
|
111
|
+
|
|
112
|
+
**Cor de status em texto:** texto e ícone sobre superfície neutra usam a camada `-emphasis` (`text-destructive-emphasis`, `text-success-emphasis`…), que passa AA nos dois temas. Nunca `text-destructive` cru para texto. Idioma: `bg-warning/10 text-warning-emphasis border-warning/20`.
|
|
113
|
+
|
|
114
|
+
**Acessibilidade:** o `Toaster` já anuncia via `aria-live`. Para um `Alert` que *aparece* em resposta a uma ação (resumo de validação), adicione `role="alert"` e mova o foco para ele.
|