@iclips/ui 3.0.0 → 4.0.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/dist/preset.css CHANGED
@@ -349,3 +349,93 @@ html {
349
349
  @apply pb-2.5;
350
350
  }
351
351
  }
352
+
353
+ /* ==========================================================
354
+ TipTap — editor (DocumentEditor / CommentThread)
355
+ Estilos que as extensoes exigem para funcionar visualmente:
356
+ o placeholder so aparece via ::before, e a lista de tarefas
357
+ precisa perder o marcador para alinhar o checkbox. As regras de
358
+ taskList nao sao presas ao .ProseMirror porque o comentario ja
359
+ postado e renderizado como HTML puro, fora do editor.
360
+ ========================================================== */
361
+
362
+ .ProseMirror p.is-editor-empty:first-child::before {
363
+ content: attr(data-placeholder);
364
+ @apply text-muted-foreground pointer-events-none float-left h-0;
365
+ }
366
+
367
+ ul[data-type="taskList"] {
368
+ @apply list-none pl-0;
369
+ }
370
+
371
+ ul[data-type="taskList"] li {
372
+ @apply flex items-start gap-2;
373
+ }
374
+
375
+ ul[data-type="taskList"] li > label {
376
+ @apply mt-1 shrink-0 select-none;
377
+ }
378
+
379
+ ul[data-type="taskList"] li > div {
380
+ @apply min-w-0 flex-1;
381
+ }
382
+
383
+ ul[data-type="taskList"] li[data-checked="true"] > div {
384
+ @apply text-muted-foreground line-through;
385
+ }
386
+
387
+ .ProseMirror mark, .prose mark {
388
+ @apply bg-primary/25 rounded-sm px-0.5;
389
+ }
390
+
391
+ /* Tabela e bloco recolhivel: so aparecem quando o consumidor liga
392
+ `features.table` / `features.details`, mas o CSS e global porque o
393
+ comentario ja postado tambem renderiza essas tags. */
394
+
395
+ .ProseMirror table {
396
+ @apply w-full table-fixed border-collapse;
397
+ }
398
+
399
+ .ProseMirror table td,
400
+ .ProseMirror table th {
401
+ @apply border-border relative min-w-24 border p-2 align-top;
402
+ }
403
+
404
+ .ProseMirror table th {
405
+ @apply bg-muted/50 text-left font-semibold;
406
+ }
407
+
408
+ .ProseMirror table .selectedCell::after {
409
+ @apply bg-primary/15 pointer-events-none absolute inset-0 z-10 content-[''];
410
+ }
411
+
412
+ .ProseMirror table .column-resize-handle {
413
+ @apply bg-primary absolute -bottom-0.5 -right-0.5 top-0 w-1 cursor-col-resize;
414
+ }
415
+
416
+ /* O tiptap nao usa <details> nativo: monta <div data-type="details"> com um
417
+ botao de toggle sem conteudo, que ganha o triangulo aqui. Aberto = .is-open */
418
+
419
+ .ProseMirror [data-type="details"] {
420
+ @apply border-border my-2 flex gap-2 rounded-md border p-2;
421
+ }
422
+
423
+ .ProseMirror [data-type="details"] > button {
424
+ @apply text-muted-foreground mt-0.5 h-4 w-4 shrink-0 cursor-pointer leading-none;
425
+ }
426
+
427
+ .ProseMirror [data-type="details"] > button::before {
428
+ content: "▸";
429
+ }
430
+
431
+ .ProseMirror [data-type="details"].is-open > button::before {
432
+ content: "▾";
433
+ }
434
+
435
+ .ProseMirror [data-type="details"] > div {
436
+ @apply min-w-0 flex-1;
437
+ }
438
+
439
+ .ProseMirror [data-type="details"] summary {
440
+ @apply cursor-pointer font-medium;
441
+ }
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`, `CommentThread`, `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.