@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/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" | "info" | "warning" | null | undefined;
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" | "info" | "warning" | null | undefined;
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`.
@@ -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.