@snksergio/design-system 0.60.0 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/ai/componentes/AlertModal.md +47 -0
- package/dist-lib/ai/componentes/AppShell.md +117 -0
- package/dist-lib/ai/componentes/Breadcrumb.md +187 -0
- package/dist-lib/ai/componentes/Button.md +116 -0
- package/dist-lib/ai/componentes/ButtonGroup.md +162 -0
- package/dist-lib/ai/componentes/CardCheckbox.md +99 -0
- package/dist-lib/ai/componentes/CardOption.md +133 -0
- package/dist-lib/ai/componentes/Chart.md +93 -0
- package/dist-lib/ai/componentes/Chip.md +68 -0
- package/dist-lib/ai/componentes/ChoroplethMap.md +118 -0
- package/dist-lib/ai/componentes/ColorPicker.md +70 -0
- package/dist-lib/ai/componentes/Combobox.md +51 -0
- package/dist-lib/ai/componentes/ConversationListItem.md +90 -0
- package/dist-lib/ai/componentes/DataList.md +111 -0
- package/dist-lib/ai/componentes/DataTable.md +867 -0
- package/dist-lib/ai/componentes/DatePicker.md +84 -0
- package/dist-lib/ai/componentes/DateSeparatorChip.md +59 -0
- package/dist-lib/ai/componentes/EmptyState.md +72 -0
- package/dist-lib/ai/componentes/FileUploadField.md +95 -0
- package/dist-lib/ai/componentes/FloatingPanel.md +118 -0
- package/dist-lib/ai/componentes/FooterTable.md +62 -0
- package/dist-lib/ai/componentes/FormField.md +110 -0
- package/dist-lib/ai/componentes/Gantt.md +552 -0
- package/dist-lib/ai/componentes/Header.md +98 -0
- package/dist-lib/ai/componentes/Icon.md +65 -0
- package/dist-lib/ai/componentes/Kanban.md +343 -0
- package/dist-lib/ai/componentes/Kpi.md +103 -0
- package/dist-lib/ai/componentes/List.md +61 -0
- package/dist-lib/ai/componentes/MarkdownText.md +59 -0
- package/dist-lib/ai/componentes/MenuSidebar.md +128 -0
- package/dist-lib/ai/componentes/MessageAck.md +53 -0
- package/dist-lib/ai/componentes/MessageBubble.md +115 -0
- package/dist-lib/ai/componentes/MessageComposer.md +80 -0
- package/dist-lib/ai/componentes/MessageVariablesPicker.md +104 -0
- package/dist-lib/ai/componentes/Modal.md +88 -0
- package/dist-lib/ai/componentes/MonthYearPicker.md +49 -0
- package/dist-lib/ai/componentes/PageHeader.md +129 -0
- package/dist-lib/ai/componentes/Panel.md +84 -0
- package/dist-lib/ai/componentes/Scheduler.md +421 -0
- package/dist-lib/ai/componentes/ScreenLoader.md +60 -0
- package/dist-lib/ai/componentes/SingleMenuSidebar.md +171 -0
- package/dist-lib/ai/componentes/Spinner.md +52 -0
- package/dist-lib/ai/componentes/Table.md +192 -0
- package/dist-lib/ai/componentes/TableToolbar.md +87 -0
- package/dist-lib/ai/componentes/TabsNavigation.md +152 -0
- package/dist-lib/ai/componentes/Toast.md +49 -0
- package/dist-lib/ai/componentes/_primitivos.md +74 -0
- package/dist-lib/ai/componentes/avatar-ig.md +181 -0
- package/dist-lib/ai/componentes/indice.json +49 -0
- package/dist-lib/ai/exemplos/dashboard/dashboard-brazil-map.ts +33 -0
- package/dist-lib/ai/exemplos/dashboard/dashboard-screen.tsx +1110 -0
- package/dist-lib/ai/exemplos/dashboard/index.ts +1 -0
- package/dist-lib/ai/global/componentes.md +217 -0
- package/dist-lib/ai/global/composicao.md +182 -0
- package/dist-lib/ai/indice.json +177 -0
- package/dist-lib/ai/lint/ds-lint-patterns.mjs +115 -0
- package/dist-lib/ai/manifest.json +16 -0
- package/dist-lib/ai/regras/design.md +88 -0
- package/dist-lib/ai/regras/temas.md +192 -0
- package/dist-lib/ai/regras-por-componente.json +103 -0
- package/dist-lib/ai/roteiros/dashboard/blueprint.md +47 -0
- package/dist-lib/ai/roteiros/dashboard/entrevista.md +62 -0
- package/dist-lib/ai/roteiros/dashboard/geracao.md +88 -0
- package/dist-lib/ai/roteiros/dashboard/roteiro.md +88 -0
- package/package.json +4 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# DatePicker — USAGE
|
|
2
|
+
|
|
3
|
+
**Categoria:** composto (Popover + Calendar). Seletor de data(s) com trigger no estilo input do DS.
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
- Campo de formulário pra escolher **uma** data (nascimento, vencimento, data de referência).
|
|
7
|
+
- Selecionar um **intervalo** de datas (período de relatório, filtro por data inicial/final).
|
|
8
|
+
- Selecionar **várias datas soltas** (não contíguas).
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
```tsx
|
|
12
|
+
import { DatePicker } from "@/components/ui/DatePicker";
|
|
13
|
+
import type { DatePickerProps, DateRange } from "@/components/ui/DatePicker";
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Variants (`mode`) — muda o shape do `value`
|
|
17
|
+
|
|
18
|
+
`DatePickerProps` é uma discriminated union por `mode`. Trocar de modo troca o TIPO de
|
|
19
|
+
`value`/`onValueChange` junto — trate como union, não como prop solta.
|
|
20
|
+
|
|
21
|
+
| `mode` | Obrigatória? | `value` | `onValueChange` | Nº de meses (default) |
|
|
22
|
+
|---|---|---|---|---|
|
|
23
|
+
| `"single"` (default) | não (`mode?: "single"`) | `Date \| undefined` | `(value: Date \| undefined) => void` | 1 |
|
|
24
|
+
| `"range"` | sim (`mode: "range"`) | `DateRange \| undefined` (`{ from: Date \| undefined; to?: Date }`, reexportado de `react-day-picker`) | `(value: DateRange \| undefined) => void` | 2 |
|
|
25
|
+
| `"multiple"` | sim (`mode: "multiple"`) | `Date[] \| undefined` | `(value: Date[] \| undefined) => void` | 1 |
|
|
26
|
+
|
|
27
|
+
Comportamento de fechamento do popover por modo:
|
|
28
|
+
- `single`: fecha ao clicar numa data.
|
|
29
|
+
- `range`: só fecha quando `from` **e** `to` estão preenchidos (clique único no primeiro dia não fecha).
|
|
30
|
+
- `multiple`: **não fecha sozinho** — o usuário fecha clicando fora.
|
|
31
|
+
|
|
32
|
+
Label do trigger por modo (não é customizável — ver Gotchas). Formatos conferidos no
|
|
33
|
+
showcase, não inferidos do código:
|
|
34
|
+
- `single`: mês por extenso — `19 de junho de 2026` (`month: "long"`).
|
|
35
|
+
- `range`: mês abreviado nas duas pontas — `10 de jul. de 2026 – 18 de ago. de 2026`
|
|
36
|
+
(`month: "short"`); com só o `from` preenchido, mostra apenas ele.
|
|
37
|
+
- `multiple`: contagem — `1 data selecionada` / `N datas selecionadas`.
|
|
38
|
+
|
|
39
|
+
## Props essenciais
|
|
40
|
+
| Prop | Tipo | Default | Descrição |
|
|
41
|
+
|---|---|---|---|
|
|
42
|
+
| `mode` | `"single" \| "range" \| "multiple"` | `"single"` | Modo de seleção — ver tabela acima pro shape de `value`. |
|
|
43
|
+
| `value` | `Date` \| `DateRange` \| `Date[]` (conforme `mode`) | — | Seleção controlada. |
|
|
44
|
+
| `onValueChange` | conforme `mode` (ver tabela acima) | — | Callback de mudança. |
|
|
45
|
+
| `placeholder` | `string` | `"Selecione a data"` (single/multiple) ou `"Selecione o período"` (range) | Texto do trigger quando nada está selecionado. |
|
|
46
|
+
| `disabled` | `boolean` | — | Desabilita o **trigger** (botão inteiro). |
|
|
47
|
+
| `align` | `"start" \| "center" \| "end"` | `"start"` | Alinhamento do `PopoverContent`. |
|
|
48
|
+
| `numberOfMonths` | `number` | `1` (single/multiple) / `2` (range) | Nº de meses exibidos no `Calendar` interno. |
|
|
49
|
+
| `className` | `string` | — | className do trigger (mesmos overrides de um input/`SelectTrigger`). |
|
|
50
|
+
|
|
51
|
+
## Exemplo mínimo
|
|
52
|
+
```tsx
|
|
53
|
+
// single (default)
|
|
54
|
+
const [date, setDate] = useState<Date>();
|
|
55
|
+
<DatePicker value={date} onValueChange={setDate} placeholder="Data de nascimento" />
|
|
56
|
+
|
|
57
|
+
// range
|
|
58
|
+
const [range, setRange] = useState<DateRange>();
|
|
59
|
+
<DatePicker mode="range" value={range} onValueChange={setRange} />
|
|
60
|
+
|
|
61
|
+
// multiple
|
|
62
|
+
const [dates, setDates] = useState<Date[]>();
|
|
63
|
+
<DatePicker mode="multiple" value={dates} onValueChange={setDates} />
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Cuidados / Gotchas
|
|
67
|
+
- **Composto sobre `Popover` + `Calendar` do DS** (`@/components/shadcn/popover` e
|
|
68
|
+
`@/components/shadcn/calendar`, este último em cima de `react-day-picker`). Copiando via
|
|
69
|
+
registry essas deps já vêm junto (`date-picker` declara `@igreen/calendar` +
|
|
70
|
+
`@igreen/popover` como `registryDependencies`); copiando manual, garanta que os dois
|
|
71
|
+
existem no consumidor antes.
|
|
72
|
+
- **`mode` muda o TIPO, não só o comportamento.** `single` é opcional (default), mas
|
|
73
|
+
`range`/`multiple` exigem a prop explícita. Trocar o modo de um DatePicker controlado
|
|
74
|
+
exige trocar o estado (`Date` → `DateRange`/`Date[]`) junto — o TS não deixa passar
|
|
75
|
+
`value` do shape errado se a discriminated union for respeitada.
|
|
76
|
+
- Em `range`, o `Calendar` interno recebe `min={1}` — sem isso o react-day-picker fecharia
|
|
77
|
+
o range já no primeiro clique (`from`/`to` iguais no mesmo dia).
|
|
78
|
+
- Em `multiple`, não há botão "Aplicar"/"Fechar" embutido — o popover some só no
|
|
79
|
+
clique-fora.
|
|
80
|
+
- **Não há pass-through de restrição de datas** (desabilitar dias específicos / limitar o
|
|
81
|
+
intervalo navegável) pro `Calendar` interno — a prop `disabled` do DatePicker desabilita
|
|
82
|
+
o trigger inteiro, não datas específicas do calendário.
|
|
83
|
+
- O label do trigger é formatado em pt-BR fixo (`toLocaleDateString("pt-BR", ...)`); não
|
|
84
|
+
existe prop de formato customizável.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# DateSeparatorChip
|
|
2
|
+
|
|
3
|
+
**Categoria:** composto (Chip + Icon + Separator). Separador **centralizado na thread** do chat/atendimento. Marcador estático — não interativo.
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
|
|
7
|
+
- **Separador de data** numa lista de mensagens (`MessagesList`): "Hoje", "Ontem", "22 jun".
|
|
8
|
+
- **Limite de conversa** (`boundary`): "Conversa encerrada" / "Conversa iniciada" — chip ladeado por duas réguas finas.
|
|
9
|
+
|
|
10
|
+
Não use para status de mensagem individual (use `Chip`) nem para divisões de seção genéricas fora da thread (use `Separator`).
|
|
11
|
+
|
|
12
|
+
## Anatomia
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
variant="date" variant="boundary"
|
|
16
|
+
┌──────────┐ ────── ┌────────────────────┐ ──────
|
|
17
|
+
│ Hoje │ │ ✓ Conversa encerrada│
|
|
18
|
+
└──────────┘ └────────────────────┘
|
|
19
|
+
(chip pílula neutra, (chip entre 2 réguas bg-border;
|
|
20
|
+
centralizado mx-auto) ícone opcional à esquerda)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Props essenciais
|
|
24
|
+
|
|
25
|
+
| Prop | Tipo | Default | Descrição |
|
|
26
|
+
|------|------|---------|-----------|
|
|
27
|
+
| `label` | `string` | — | **Obrigatório.** Texto centralizado da pílula. |
|
|
28
|
+
| `variant` | `"date" \| "boundary"` | `"date"` | `date` = só o chip; `boundary` = chip entre 2 réguas. |
|
|
29
|
+
| `icon` | `IconName` | — | Ícone opcional à esquerda do label (ex: `line-check-circle` em boundary). |
|
|
30
|
+
| `className` | `string` | — | className do container (root). |
|
|
31
|
+
|
|
32
|
+
## Exemplo mínimo
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { DateSeparatorChip } from "@snksergio/design-system";
|
|
36
|
+
|
|
37
|
+
// Separador de data na MessagesList
|
|
38
|
+
<DateSeparatorChip label="Hoje" />
|
|
39
|
+
|
|
40
|
+
// Limite de conversa
|
|
41
|
+
<DateSeparatorChip
|
|
42
|
+
variant="boundary"
|
|
43
|
+
icon="line-check-circle"
|
|
44
|
+
label="Conversa encerrada"
|
|
45
|
+
/>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Variants
|
|
49
|
+
|
|
50
|
+
| Variant | Valores | Efeito |
|
|
51
|
+
|---------|---------|--------|
|
|
52
|
+
| `variant` | `date` · `boundary` | `date` = chip centralizado isolado; `boundary` = chip ladeado por 2 réguas finas (`bg-border` do Separator) ocupando o espaço lateral |
|
|
53
|
+
|
|
54
|
+
## Gotchas
|
|
55
|
+
|
|
56
|
+
- **Composição pura:** reusa `Chip` (color `neutral`, variant `soft`, size `sm` → pílula `rounded-radius-full`, `text-caption-sm`), `Icon` (`size="xs"` = 12px) e `Separator` do shadcn. Não reescreve nenhum deles.
|
|
57
|
+
- **Réguas:** as réguas de `boundary` são `<Separator>` do shadcn com só `flex-1` (slot `rule`) pra esticar — a cor `bg-border` já vem do próprio Separator, sem override; centralização do chip via `mx-auto` + `gap-gp-md` lateral.
|
|
58
|
+
- **Estático:** sem foco/disabled — é um marcador, não um controle. Renderiza `role="separator"` + `aria-label={label}`.
|
|
59
|
+
- **Acessibilidade do ícone:** o `Icon` aqui é decorativo (sem `title`) — o significado já vem no `label` lido pelo `aria-label` do separador.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# EmptyState
|
|
2
|
+
|
|
3
|
+
**Categoria:** composto (Icon/lucide + Button). Estado vazio genérico, reusável no app inteiro — sem dados, busca sem resultado, conversa não selecionada, lista/inbox vazia, erro de carregamento, etc.
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
|
|
7
|
+
- Uma área não tem conteúdo a exibir e você quer comunicar o porquê + (opcional) uma ação para sair do vazio.
|
|
8
|
+
- Tela de chat sem conversa selecionada, tabela/lista sem itens, resultado de busca/filtro vazio.
|
|
9
|
+
|
|
10
|
+
Não use para erros bloqueantes de página inteira com retry de sistema (use um padrão de erro dedicado) nem para loading (use skeleton/spinner).
|
|
11
|
+
|
|
12
|
+
## Anatomia
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
[ ícone ] ← size-icon-xl (sm) / 2xl (md,lg), cor fg-subtle
|
|
16
|
+
Título ← text-title-sm (sm,md) / md (lg), fg-strong
|
|
17
|
+
Descrição opcional ← body-sm, fg-muted, max-w 360px
|
|
18
|
+
[ Button ] ← ação opcional (mt de respiro)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Tudo centralizado vertical e horizontalmente, texto centralizado.
|
|
22
|
+
|
|
23
|
+
## Props essenciais
|
|
24
|
+
|
|
25
|
+
| Prop | Tipo | Default | Descrição |
|
|
26
|
+
|------|------|---------|-----------|
|
|
27
|
+
| `title` | `string` | — | **Obrigatório.** Título principal. |
|
|
28
|
+
| `icon` | `LucideIcon \| ReactNode` | — | Componente lucide (ex: `Inbox`) **ou** node (Icon do DS, ilustração). Componente é dimensionado pelo `size`; node herda o tamanho via `[&_svg]:size-full`. |
|
|
29
|
+
| `description` | `string` | — | Texto auxiliar sob o título. |
|
|
30
|
+
| `action` | `{ label, onClick, color?, variant? } \| ReactNode` | — | Objeto vira um `<Button>` do DS; ou passe um node custom (2 botões, link...). |
|
|
31
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Tamanho do ícone + tipografia do título + size do Button da ação. |
|
|
32
|
+
| `className` | `string` | — | className do container. |
|
|
33
|
+
|
|
34
|
+
## Variants
|
|
35
|
+
|
|
36
|
+
| `size` | Ícone | Título | Button da ação |
|
|
37
|
+
|--------|-------|--------|----------------|
|
|
38
|
+
| `sm` | `size-icon-xl` (32px) | `text-title-sm` | `sm` |
|
|
39
|
+
| `md` | `size-icon-2xl` (40px) | `text-title-sm` | `md` |
|
|
40
|
+
| `lg` | `size-icon-2xl` (40px) | `text-title-md` | `lg` |
|
|
41
|
+
|
|
42
|
+
## Exemplo mínimo
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { Inbox } from "lucide-react";
|
|
46
|
+
import { EmptyState } from "@/components/ui/EmptyState";
|
|
47
|
+
|
|
48
|
+
// Lista vazia com ação
|
|
49
|
+
<EmptyState
|
|
50
|
+
icon={Inbox}
|
|
51
|
+
title="Nenhuma conversa selecionada"
|
|
52
|
+
description="Selecione uma conversa ao lado para visualizar as mensagens."
|
|
53
|
+
action={{ label: "Iniciar atendimento", onClick: handleStart }}
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
// Só ícone + título (busca sem resultado)
|
|
57
|
+
<EmptyState size="sm" icon={Search} title="Nenhum resultado encontrado" />
|
|
58
|
+
|
|
59
|
+
// Ícone custom via Icon do DS + ação custom (node)
|
|
60
|
+
<EmptyState
|
|
61
|
+
icon={<Icon name="line-mail" />}
|
|
62
|
+
title="Caixa de entrada vazia"
|
|
63
|
+
action={<MyCustomButtons />}
|
|
64
|
+
/>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Gotchas / cuidados
|
|
68
|
+
|
|
69
|
+
- **`icon` aceita componente OU node.** Passe a referência do componente lucide (`icon={Inbox}`, sem `<>`), que ele é instanciado e dimensionado pelo `size`. Para um `<Icon>` do DS ou ilustração, passe o elemento (`icon={<Icon name="..." />}`) — o wrapper aplica cor `fg-subtle` e o SVG ocupa o tamanho via `[&_svg]:size-full`.
|
|
70
|
+
- **`action` é discriminada em runtime:** objeto plano com `label`+`onClick` vira Button; qualquer element React é renderizado como está. Para customizar a cor/variante do Button, use `{ label, onClick, color, variant }`.
|
|
71
|
+
- Componente declarativo de **display** — não tem foco próprio nem `disabled`. O único elemento interativo é o Button da ação (já traz o Padrão 1 de foco do DS).
|
|
72
|
+
- O título usa `<h3>`; garanta hierarquia de headings coerente na página que o consome.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# FileUploadField — USAGE
|
|
2
|
+
|
|
3
|
+
Captura de **um** arquivo com preview. Componente **dumb**: só captura o `File` e
|
|
4
|
+
mostra preview — o **consumidor faz o upload**. Categoria: form / data-input.
|
|
5
|
+
|
|
6
|
+
Composto de `FormField` (label/estado/erro) + `Button` + `Chip` + `Icon` + `tv()`.
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
- Anexar avatar, logo, comprovante, documento único em um formulário.
|
|
11
|
+
- Quando o upload é responsabilidade do consumer (envia o `File` pra API depois).
|
|
12
|
+
- Para **múltiplos** arquivos ou drag & drop, NÃO use este (single-file; D&D é fase 2).
|
|
13
|
+
|
|
14
|
+
## Import
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { FileUploadField } from "@/components/ui/FileUploadField";
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Props essenciais
|
|
21
|
+
|
|
22
|
+
| Prop | Tipo | Default | Descrição |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `value` | `File \| string \| null` | — (req) | `File` recém-selecionado, `string` (URL hospedada) ou `null` (vazio) |
|
|
25
|
+
| `onChange` | `(file: File \| null) => void` | — (req) | Selecionar → `File`; remover → `null` |
|
|
26
|
+
| `accept` | `string` | — | Filtro do seletor + validação (`"image/*"`, `".pdf,.png"`) |
|
|
27
|
+
| `maxSizeMB` | `number` | — | Tamanho máximo; acima → rejeitado via `onError("size")` |
|
|
28
|
+
| `preview` | `"image" \| "file" \| "auto"` | `"auto"` | Modo de preview; `auto` infere por accept/MIME |
|
|
29
|
+
| `fileName` | `string` | — | Nome exibido quando `value` é uma URL |
|
|
30
|
+
| `label` | `string` | — | Label (renderizado pelo FormField) |
|
|
31
|
+
| `required` | `boolean` | — | Asterisco no label |
|
|
32
|
+
| `state` | `"default" \| "error" \| "warning" \| "success"` | `"default"` | Estado semântico do FormField |
|
|
33
|
+
| `errorMessage` | `string` | — | Mensagem quando `state="error"` |
|
|
34
|
+
| `helperText` | `string` | — | Texto auxiliar abaixo do campo |
|
|
35
|
+
| `onError` | `(reason: "type" \| "size") => void` | — | Disparado quando arquivo é rejeitado |
|
|
36
|
+
| `disabled` | `boolean` | — | Desabilita dropzone + remover |
|
|
37
|
+
| `id` | `string` | — | id do input (linka label↔input) |
|
|
38
|
+
| `className` | `string` | — | className do container (FormField) |
|
|
39
|
+
|
|
40
|
+
## Estados visuais
|
|
41
|
+
|
|
42
|
+
| value | Render |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `null` | Dropzone `<button>` full-width (`min-h-form-xl`, dashed) com ícone + "Clique para anexar" + hint accept/maxSize |
|
|
45
|
+
| `File`/URL imagem | Row: thumbnail (`size-icon-2xl`, object-cover) + botão remover |
|
|
46
|
+
| `File`/URL arquivo | Row: Chip soft (ícone file + nome truncate) + botão remover |
|
|
47
|
+
|
|
48
|
+
## Exemplo mínimo
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
const [file, setFile] = useState<File | null>(null);
|
|
52
|
+
|
|
53
|
+
<FileUploadField
|
|
54
|
+
label="Comprovante"
|
|
55
|
+
required
|
|
56
|
+
value={file}
|
|
57
|
+
onChange={setFile}
|
|
58
|
+
accept="image/*,.pdf"
|
|
59
|
+
maxSizeMB={5}
|
|
60
|
+
onError={(reason) =>
|
|
61
|
+
toast.error(reason === "size" ? "Arquivo muito grande" : "Tipo inválido")
|
|
62
|
+
}
|
|
63
|
+
/>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Editar registro existente (value como URL)
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<FileUploadField
|
|
70
|
+
label="Logo"
|
|
71
|
+
value={logoUrl} // string (URL já hospedada)
|
|
72
|
+
fileName="logo.png"
|
|
73
|
+
onChange={(f) => setNewLogo(f)} // null = remover, File = trocar
|
|
74
|
+
accept="image/*"
|
|
75
|
+
preview="image"
|
|
76
|
+
/>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Gotchas / cuidados
|
|
80
|
+
|
|
81
|
+
- **Dumb component**: não envia nada à rede. O `onChange(File)` te dá o `File`;
|
|
82
|
+
faça `FormData`/upload no consumer e persista a URL retornada.
|
|
83
|
+
- Arquivo rejeitado (tipo/tamanho) **não** vira `value` — só dispara `onError`.
|
|
84
|
+
Para feedback visual no campo, controle `state="error"` + `errorMessage` no consumer.
|
|
85
|
+
- Preview de `File` usa `URL.createObjectURL` (revogado automaticamente). Para URL
|
|
86
|
+
string, o `src` é o próprio value.
|
|
87
|
+
- Single-file: o input não tem `multiple`. Drag & drop é fase 2 (API preservada).
|
|
88
|
+
- A11y: dropzone é `<button>` (Enter/Space abrem o seletor); input file hidden com
|
|
89
|
+
`id` do FormField; thumbnail tem `alt`; remover é icon-only com `aria-label`.
|
|
90
|
+
|
|
91
|
+
## Fonte de verdade
|
|
92
|
+
|
|
93
|
+
- Estilos: `file-upload-field.styles.ts` (tv())
|
|
94
|
+
- Lógica: `file-upload-field.tsx`
|
|
95
|
+
- Tipos: `file-upload-field.types.ts`
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# FloatingPanel — USAGE
|
|
2
|
+
|
|
3
|
+
<!-- ds:regras
|
|
4
|
+
- painel de DETALHE → siga o bloco `dsgreen-paneldetail-1`, que é o **padrão**. As variações são opt-in: use `-2` (tarefa com abas) ou `-3` (com tabela, `size="xl"`) **só se o usuário citar o ID**
|
|
5
|
+
- em todos: identidade ou contexto no `titleSlot`, ações de ícone `soft` + `aria-label` no `headerActions`, ação primária no `footer` — nunca botão de ação solto no corpo
|
|
6
|
+
- `bodyPadded={false}` quando usar `FloatingPanelSection` (a section gerencia padding e divisória full-width)
|
|
7
|
+
- é REDIMENSIONÁVEL em runtime — nenhuma largura escrita na mão acompanha o arrasto
|
|
8
|
+
- aba dentro dele → `<Tabs fullWidth>` na variante default; `line` aqui vira trilho curto
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
Drawer card flutuante non-modal — resizável, maximizável, coexiste com página atrás (sem backdrop).
|
|
12
|
+
|
|
13
|
+
## Quando usar
|
|
14
|
+
- Detail panel que precisa ficar visível enquanto user interage com lista atrás
|
|
15
|
+
- Side-drawer leve sem bloquear navegação
|
|
16
|
+
- Painel de propriedades estilo Figma/Notion
|
|
17
|
+
|
|
18
|
+
## Import
|
|
19
|
+
```tsx
|
|
20
|
+
import {
|
|
21
|
+
FloatingPanel,
|
|
22
|
+
FloatingPanelSection, // seção colapsável (detail panel)
|
|
23
|
+
FloatingPanelField, // linha label : valor
|
|
24
|
+
} from "@/components/ui/FloatingPanel";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Variants
|
|
28
|
+
| Variant | Valores | Default | Quando |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `side` | left / right | right | Lado de ancoragem |
|
|
31
|
+
| `size` | sm / md / lg / xl / number (px) | md | sm=320, md=400, lg=560, xl=720px. `number` = largura custom em px; com `resizable`, vira a largura inicial |
|
|
32
|
+
|
|
33
|
+
## Props essenciais
|
|
34
|
+
| Prop | Tipo | Função |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `open` | boolean | Visibilidade |
|
|
37
|
+
| `onOpenChange` | (open: boolean) => void | Callback de fechamento |
|
|
38
|
+
| `title` | string | Header title |
|
|
39
|
+
| `description` | string | Header subtitle |
|
|
40
|
+
| `titleIcon` | LucideIcon | Ícone à esquerda do título |
|
|
41
|
+
| `hideClose` | boolean | Esconde o botão X de fechar (default `false`) |
|
|
42
|
+
| `resizable` | boolean | Habilita drag-resize (desabilitado em mobile) |
|
|
43
|
+
| `resizableMinWidth` / `resizableMaxWidth` | number | Bounds do resize em px (defaults `320` / `800`). São **px cegos à viewport** — quem impede o estouro é o teto `md:max-w-[calc(100vw-48px)]` do próprio painel, que mantém o gutter de 24px dos dois lados. Antes dele, props default numa janela de 800px punham a borda esquerda em **-24px** (800 de largura + 24 de gutter = 824 necessários) |
|
|
44
|
+
| `resizableStorageKey` | string | Chave do localStorage pra persistir width entre sessões |
|
|
45
|
+
| `maximizable` | boolean | Botão de expandir pra fullscreen |
|
|
46
|
+
| `headerActions` | ReactNode | Slots no header |
|
|
47
|
+
| `footer` | ReactNode | Slot do footer sticky |
|
|
48
|
+
| `titleSlot` | ReactNode | Override total do header (avatar + nome custom) |
|
|
49
|
+
| `bodyPadded` | boolean | Padding interno padrão do body (gutter 18px). **Default `true`** — conteúdo livre já respira. Use `false` com `<FloatingPanelSection>` (sections gerenciam o próprio padding edge-to-edge) |
|
|
50
|
+
|
|
51
|
+
## Exemplo mínimo (conteúdo livre — padding automático)
|
|
52
|
+
```tsx
|
|
53
|
+
<FloatingPanel
|
|
54
|
+
open={panelOpen}
|
|
55
|
+
onOpenChange={setPanelOpen}
|
|
56
|
+
side="right"
|
|
57
|
+
size="md"
|
|
58
|
+
title="Detalhes do cliente"
|
|
59
|
+
description="Última edição há 2h"
|
|
60
|
+
resizable
|
|
61
|
+
maximizable
|
|
62
|
+
footer={<><Button variant="ghost">Cancelar</Button><Button>Salvar</Button></>}
|
|
63
|
+
>
|
|
64
|
+
{/* bodyPadded=true (default) → conteúdo já tem gutter, não precisa de p-* manual */}
|
|
65
|
+
<ClientDetails />
|
|
66
|
+
</FloatingPanel>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Detail panel padrão → o bloco `dsgreen-paneldetail-1`
|
|
70
|
+
|
|
71
|
+
**Painel de detalhe de registro tem estrutura definida, e ela é referenciável por ID.** A
|
|
72
|
+
composição inteira (com fixture, medições e o porquê de cada decisão) vive em
|
|
73
|
+
`src/blocks/paneldetail/` e é renderizada em `#/blocks-paneldetail`. Cite o ID em vez de
|
|
74
|
+
recompor: `use a referência dsgreen-paneldetail-1 no painel de detalhe do pedido`.
|
|
75
|
+
|
|
76
|
+
As quatro zonas, que são o que a IA erra quando compõe do zero — ela joga tudo no corpo:
|
|
77
|
+
|
|
78
|
+
| zona | o que vai | por que |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `titleSlot` | avatar + nome + código · Chip de status | responde "de quem é este painel"; fica fixo no scroll |
|
|
81
|
+
| `headerActions` | 1–2 ações de ícone, **sempre `variant="soft"`** | `ghost` no meio da fileira fica sem container e lê como desabilitada ao lado do maximize/close, que são `soft` |
|
|
82
|
+
| corpo, 1ª seção | métricas em **cards compactos** (não `Kpi`) | responde "como este registro está?" — vem antes dos campos, que respondem "quais são os dados" |
|
|
83
|
+
| corpo, resto | campos em `FloatingPanelSection` por assunto | o colapso é o que permite 20 campos sem obrigar a rolar 20 |
|
|
84
|
+
| `footer` | Fechar + ação primária | a ação que fecha a tarefa, sempre alcançável |
|
|
85
|
+
|
|
86
|
+
⛔ **Não use aba aqui.** Se o corpo já é pilha de seções colapsáveis, o colapso **é** o
|
|
87
|
+
mecanismo de esconder — ter os dois faz o usuário procurar o dado em dois lugares. Recorte
|
|
88
|
+
volumoso de verdade (extrato de 200 linhas) não é aba nem seção: é outra tela.
|
|
89
|
+
|
|
90
|
+
### Anatomia mínima
|
|
91
|
+
|
|
92
|
+
Use `bodyPadded={false}` (as sections têm padding + divisória própria):
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
<FloatingPanel open={open} onOpenChange={setOpen} bodyPadded={false} titleSlot={<HeaderCustom />}>
|
|
96
|
+
<FloatingPanelSection title="Contato"> {/* colapsável (default) */}
|
|
97
|
+
<FloatingPanelField label="Email" value={<a href={...}>{email}</a>} />
|
|
98
|
+
<FloatingPanelField label="Telefone" value={phone} />
|
|
99
|
+
</FloatingPanelSection>
|
|
100
|
+
<FloatingPanelSection title="Financeiro" defaultOpen={false}>
|
|
101
|
+
<FloatingPanelField label="Saldo" value={formatBRL(saldo)} />
|
|
102
|
+
</FloatingPanelSection>
|
|
103
|
+
</FloatingPanel>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- `FloatingPanelSection` — props: `title`, `collapsible` (default `true`), `defaultOpen` (default `true`).
|
|
107
|
+
- `FloatingPanelField` — props: `label`, `value` (fallback "—" quando vazio).
|
|
108
|
+
|
|
109
|
+
## Cuidados / Gotchas
|
|
110
|
+
- **Aba dentro do FloatingPanel** → `<Tabs fullWidth>` com a variante **default** (`segmented`). Aqui é o caso mais forte da prop: o painel é **redimensionável em runtime** (`sm` 320 · `md` 400 · `lg` 560 · `xl` 720, e o usuário arrasta), então nenhuma largura escrita na mão acompanha — `fullWidth` distribui os triggers e a aba segue o arrasto.
|
|
111
|
+
|
|
112
|
+
- Renderizado via **portal em document.body** — escapa de overflow/transform ancestrais
|
|
113
|
+
- Em mobile (<md) vira **sheet bottom-up colado nas bordas** do device: flush nas laterais + bottom, só cantos superiores arredondados, sem outline/shadow, `max-height: 92vh`
|
|
114
|
+
- **Backdrop só em mobile** — scrim suave (toque fora fecha). No **desktop segue non-modal** (sem backdrop, página atrás clicável). Pra modal full use `<Modal>` ou `<AlertModal>`
|
|
115
|
+
- **Body com scroll automático** (`overflow-y-auto` + `min-h-0`) — header/footer ficam fixos, conteúdo longo rola
|
|
116
|
+
- **Footer fluido**: botões crescem lado a lado e **empilham quando não cabem** (`flex-wrap` + `flex-1` + `min-w-140px`). Não precisa passar `fullWidth` nos Buttons
|
|
117
|
+
- `maximizable=true` adiciona botão de expandir; estado controlado internamente — `defaultMaximized` (default `false`) inicia maximizado
|
|
118
|
+
- **ESC fecha por padrão** (`closeOnEscape` default `true`) — passe `false` pra desativar
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# FooterTable — USAGE
|
|
2
|
+
|
|
3
|
+
Footer de tabela com paginação + page-size select + range display + selection count.
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
- Rodapé de qualquer `<Table>` ou `<DataTable>` (já é embutido no DataTable)
|
|
7
|
+
- Listas paginadas com seleção múltipla
|
|
8
|
+
|
|
9
|
+
## Import
|
|
10
|
+
```tsx
|
|
11
|
+
import { FooterTable } from "@/components/ui/FooterTable";
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Props essenciais
|
|
15
|
+
| Prop | Tipo | Default | Função |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `totalCount` | number | — | Total de registros |
|
|
18
|
+
| `page` | number | — | Página atual (1-indexed) |
|
|
19
|
+
| `pageSize` | number | — | Tamanho da página atual |
|
|
20
|
+
| `onPageChange` | (page: number) => void | — | Callback de paginação |
|
|
21
|
+
| `onPageSizeChange` | (size: number) => void | — | Callback de troca de page-size |
|
|
22
|
+
| `pageSizeOptions` | number[] | [10, 25, 50, 100] | Opções do select |
|
|
23
|
+
| `selectionCount` | number | 0 | Quantos selecionados (acrescenta "· N selecionado(s)" ao lado do range) |
|
|
24
|
+
| `pageSizeLabel` | string | "Linhas" | Label do select de page-size |
|
|
25
|
+
| `rowLabel` | string | "rows" | Sufixo do range (ex "registros") |
|
|
26
|
+
| `locale` | string | "pt-BR" | Locale pra formatar números do range |
|
|
27
|
+
| `hidePageSize` | boolean | false | Esconde select de page-size |
|
|
28
|
+
| `hideRange` | boolean | false | Esconde "1–10 de 87 rows" |
|
|
29
|
+
| `hideFirstLast` | boolean | false | Esconde botões « » |
|
|
30
|
+
|
|
31
|
+
## Exemplo mínimo
|
|
32
|
+
```tsx
|
|
33
|
+
<FooterTable
|
|
34
|
+
totalCount={87}
|
|
35
|
+
page={currentPage}
|
|
36
|
+
pageSize={10}
|
|
37
|
+
onPageChange={setPage}
|
|
38
|
+
onPageSizeChange={setPageSize}
|
|
39
|
+
selectionCount={selectedIds.length}
|
|
40
|
+
/>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## FooterTableSkeleton
|
|
44
|
+
Estado loading do footer — usado pelo DataTable enquanto a primeira chamada de `fetchData` não retornou `totalCount`. Mantém a mesma silhueta do footer real pra evitar layout shift.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { FooterTableSkeleton } from "@/components/ui/FooterTable";
|
|
48
|
+
|
|
49
|
+
<FooterTableSkeleton pageButtonCount={5} />
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| Prop | Tipo | Default | Função |
|
|
53
|
+
|---|---|---|---|
|
|
54
|
+
| `pageButtonCount` | number | 5 | Quantos botões de página simular |
|
|
55
|
+
| `className` | string | — | className extra no `<footer>` |
|
|
56
|
+
|
|
57
|
+
## Cuidados / Gotchas
|
|
58
|
+
- Calcula range "1–10 de 87 rows" automaticamente (sufixo customizável via `rowLabel`)
|
|
59
|
+
- Empilha vertical em mobile (`flex-col <sm`) — layout fixo, sem custom
|
|
60
|
+
- `selectionCount > 0` acrescenta "· N selecionado(s)" ao lado do range — NÃO substitui o range; pra esconder o range use `hideRange`
|
|
61
|
+
- Default `rowLabel = "rows"` produz texto misto ("1–10 de 87 rows") — passe `rowLabel="registros"` pra texto 100% pt-BR
|
|
62
|
+
- Pra usar fora de tabela, pode ser standalone — props são puramente paginação
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# FormField — USAGE
|
|
2
|
+
|
|
3
|
+
Container de form com label + field + mensagem de validação (error/warning/success). Inclui wrappers "one-shot" (`FormFieldInput`, `FormFieldSelect`, etc) — a forma recomendada de consumo em forms (L-023).
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
|
|
7
|
+
- Qualquer campo de formulário que tenha label + input + validação
|
|
8
|
+
- Garantir spacing e tipografia consistente entre campos
|
|
9
|
+
- NUNCA escrever `<label>` raw com classes manuais em forms (L-023)
|
|
10
|
+
|
|
11
|
+
## Import
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import {
|
|
15
|
+
FormField, // base (children render-prop) — para widgets custom
|
|
16
|
+
FormFieldInput, // wrappers one-shot (recomendados)
|
|
17
|
+
FormFieldTextarea,
|
|
18
|
+
FormFieldSelect,
|
|
19
|
+
FormFieldCheckbox,
|
|
20
|
+
FormFieldSwitch,
|
|
21
|
+
} from "@/components/ui/FormField";
|
|
22
|
+
import type { FieldState, FormFieldSelectOption } from "@/components/ui/FormField";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Props essenciais
|
|
26
|
+
|
|
27
|
+
### `FormFieldBaseProps` — comuns a todos
|
|
28
|
+
|
|
29
|
+
| Prop | Tipo | Default | Função |
|
|
30
|
+
| ---------------- | ------------------------------------------------- | ------------- | ------------------------------------------------------------ |
|
|
31
|
+
| `label` | `string` | — | Label acima do field (Checkbox/Switch: `ReactNode`, inline) |
|
|
32
|
+
| `required` | boolean | `false` | Asterisco vermelho ao lado do label (só visual) |
|
|
33
|
+
| `helperText` | `ReactNode` | — | Texto auxiliar abaixo do field |
|
|
34
|
+
| `state` | `"default" \| "error" \| "warning" \| "success"` | `"default"` | Estado semântico — afeta borda do field e mensagem exibida |
|
|
35
|
+
| `errorMessage` | `ReactNode` | — | Exibida quando `state="error"` (substitui o helperText) |
|
|
36
|
+
| `warningMessage` | `ReactNode` | — | Exibida quando `state="warning"` |
|
|
37
|
+
| `successMessage` | `ReactNode` | — | Exibida quando `state="success"` |
|
|
38
|
+
| `id` | `string` | auto (`useId`) | Linka label↔input via `htmlFor` |
|
|
39
|
+
| `className` | `string` | — | className do container externo |
|
|
40
|
+
|
|
41
|
+
### Específicas do `FormField` (base)
|
|
42
|
+
|
|
43
|
+
| Prop | Tipo | Função |
|
|
44
|
+
| ----------- | ----------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
45
|
+
| `children` | `(ctx: { id: string; state: FieldState }) => ReactNode` | **Render-prop obrigatório** — recebe o id gerado + state pra repassar ao field |
|
|
46
|
+
| `hideLabel` | boolean | Esconde o label (field self-contained, ex: Checkbox com label inline) |
|
|
47
|
+
| `disabled` | boolean | Esmaece SÓ o label (opacity-50) — o field filho precisa do próprio `disabled` |
|
|
48
|
+
|
|
49
|
+
### Wrappers one-shot (recomendados — L-023)
|
|
50
|
+
|
|
51
|
+
| Componente | Field interno | Extras |
|
|
52
|
+
| ------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
53
|
+
| `FormFieldInput` | `Input` / `InputGroup` (shadcn) | `startAddon` / `endAddon` (`ReactNode`; string vira `InputGroupText`, ex `"R$"`) + props de `Input` |
|
|
54
|
+
| `FormFieldTextarea` | `Textarea` (shadcn) | props de `Textarea` |
|
|
55
|
+
| `FormFieldSelect` | `Select` (radix/shadcn) | `options: FormFieldSelectOption[]` (`{ value, label, disabled? }`), `placeholder`, `value`, `defaultValue`, `onValueChange`, `disabled`, `triggerClassName`, `contentClassName` |
|
|
56
|
+
| `FormFieldCheckbox` | `Checkbox` (shadcn) | label inline à direita do checkbox; props do Radix `Checkbox.Root` |
|
|
57
|
+
| `FormFieldSwitch` | `Switch` (shadcn) | label inline; `switchPosition?: "start" \| "end"` (default `"end"`); props do Radix `Switch.Root` |
|
|
58
|
+
|
|
59
|
+
## Variants (slots internos de estilo)
|
|
60
|
+
|
|
61
|
+
| Slot | Variant | Valores | Default |
|
|
62
|
+
| --------------------------- | ---------- | ------------------------------------ | ------- |
|
|
63
|
+
| Label (`formFieldLabel`) | `disabled` | true/false | false |
|
|
64
|
+
| Message (`formFieldMessage`) | `state` | default / error / warning / success | default |
|
|
65
|
+
|
|
66
|
+
O container (`formFieldRoot`) não tem variants — é base-only (`flex flex-col gap-[7px] w-full`).
|
|
67
|
+
|
|
68
|
+
## Exemplo mínimo
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// Wrapper one-shot — forma recomendada
|
|
72
|
+
<FormFieldInput
|
|
73
|
+
label="Email"
|
|
74
|
+
required
|
|
75
|
+
type="email"
|
|
76
|
+
state="error"
|
|
77
|
+
errorMessage="Email inválido"
|
|
78
|
+
value={email}
|
|
79
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
// Com addons
|
|
83
|
+
<FormFieldInput label="Valor" startAddon="R$" placeholder="0,00" />
|
|
84
|
+
|
|
85
|
+
// Select
|
|
86
|
+
<FormFieldSelect
|
|
87
|
+
label="País"
|
|
88
|
+
placeholder="Selecione..."
|
|
89
|
+
options={[
|
|
90
|
+
{ value: "br", label: "Brasil" },
|
|
91
|
+
{ value: "us", label: "Estados Unidos" },
|
|
92
|
+
]}
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
// Base com widget custom — children é render-prop
|
|
96
|
+
<FormField label="Email" required state="error" errorMessage="Email inválido">
|
|
97
|
+
{({ id, state }) => <Input id={id} state={state} type="email" />}
|
|
98
|
+
</FormField>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Cuidados / Gotchas
|
|
102
|
+
|
|
103
|
+
- `children` do `FormField` base é **função** (render-prop), não JSX direto — passar ReactNode é erro de tipo e quebra em runtime
|
|
104
|
+
- NÃO existem componentes `FormFieldLabel`/`FormFieldMessage` — label e mensagem são props (`label`, `errorMessage`, `helperText`...); esses nomes existem só como slots internos em `form-field.styles.ts`
|
|
105
|
+
- Prioridade da mensagem: error → warning → success → helperText. A mensagem do state só aparece se a prop correspondente (`errorMessage`, etc) for passada
|
|
106
|
+
- O `state` é propagado ao field via argumento do render-prop (`ctx.state`) — não há context; em widget custom, repasse `id` e `state` manualmente
|
|
107
|
+
- `disabled` no `FormField` base esmaece apenas o label — passe `disabled` também ao field. Os wrappers (`FormFieldInput`, etc) propagam automaticamente
|
|
108
|
+
- Mensagem com `state="error"` recebe `role="alert"` (acessibilidade)
|
|
109
|
+
- Spacing label/field/message é do container — não adicionar margins customizados; entre fields empilhados use `gap-form-gap` (L-024)
|
|
110
|
+
- `required` é só visual — validação real é responsabilidade do consumer
|