@celke/kole-react 0.5.0 → 0.6.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.
Files changed (69) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +968 -638
  3. package/dist/components/Avaliacao/Avaliacao.d.ts +1 -1
  4. package/dist/components/Avatar/GrupoAvatares.d.ts +16 -0
  5. package/dist/components/BlocoCodigo/BlocoCodigo.d.ts +15 -0
  6. package/dist/components/BotaoFechar/BotaoFechar.d.ts +1 -1
  7. package/dist/components/Busca/Busca.d.ts +1 -1
  8. package/dist/components/Calendario/Calendario.d.ts +100 -0
  9. package/dist/components/Calendario/MiniCalendario.d.ts +27 -0
  10. package/dist/components/Calendario/datasCalendario.d.ts +27 -0
  11. package/dist/components/Calendario/repetirEvento.d.ts +26 -0
  12. package/dist/components/Campo/Campo.d.ts +1 -0
  13. package/dist/components/CampoBusca/CampoBusca.d.ts +1 -1
  14. package/dist/components/CampoBusca/CampoCidade.d.ts +1 -1
  15. package/dist/components/CampoMensagem/CampoMensagem.d.ts +20 -1
  16. package/dist/components/Carregando/Carregando.d.ts +1 -1
  17. package/dist/components/ChatFlutuante/ChatFlutuante.d.ts +31 -0
  18. package/dist/components/Conversa/Conversa.d.ts +1 -1
  19. package/dist/components/Estrutura/Estrutura.d.ts +13 -2
  20. package/dist/components/Form/Form.d.ts +1 -1
  21. package/dist/components/Girador/Girador.d.ts +1 -1
  22. package/dist/components/Grafico/GraficoRosca.d.ts +31 -0
  23. package/dist/components/Grafico/MolduraGrafico.d.ts +36 -1
  24. package/dist/components/Grafico/escalas.d.ts +3 -0
  25. package/dist/components/InicioConversa/InicioConversa.d.ts +29 -0
  26. package/dist/components/Kanban/CartaoKanban.d.ts +35 -0
  27. package/dist/components/Kanban/Kanban.d.ts +62 -0
  28. package/dist/components/Kanban/Pecas.d.ts +30 -0
  29. package/dist/components/Kanban/tipos.d.ts +35 -0
  30. package/dist/components/ListaCards/ListaCards.d.ts +1 -1
  31. package/dist/components/ListaConversas/ListaConversas.d.ts +40 -0
  32. package/dist/components/ListaTarefas/ListaTarefas.d.ts +52 -0
  33. package/dist/components/Mensagem/Mensagem.d.ts +1 -1
  34. package/dist/components/NavegadorPeriodo/NavegadorPeriodo.d.ts +28 -0
  35. package/dist/components/Picklist/Picklist.d.ts +1 -1
  36. package/dist/components/Preferencias/Preferencias.d.ts +25 -2
  37. package/dist/components/QuadroArrastavel/QuadroArrastavel.d.ts +59 -6
  38. package/dist/components/Referencias/Referencias.d.ts +1 -1
  39. package/dist/components/SeletorContexto/SeletorContexto.d.ts +32 -0
  40. package/dist/components/SeletorIdioma/Bandeira.d.ts +10 -0
  41. package/dist/components/SeletorIdioma/SeletorIdioma.d.ts +23 -0
  42. package/dist/components/Site/CampoPesquisa.d.ts +1 -1
  43. package/dist/components/Site/CardPlano.d.ts +1 -1
  44. package/dist/components/Site/Newsletter.d.ts +1 -1
  45. package/dist/components/Site/SumarioPagina.d.ts +1 -1
  46. package/dist/components/Tabela/Tabela.d.ts +1 -1
  47. package/dist/components/TelaLogin/TelaLogin.d.ts +1 -1
  48. package/dist/components/Valor/Valor.d.ts +53 -0
  49. package/dist/estilos.css +1 -1
  50. package/dist/i18n/contexto.d.ts +15 -0
  51. package/dist/i18n/en.d.ts +3 -0
  52. package/dist/i18n/es.d.ts +3 -0
  53. package/dist/i18n/idiomas.d.ts +41 -0
  54. package/dist/i18n/pt-BR.d.ts +545 -0
  55. package/dist/i18n/textos.d.ts +17 -0
  56. package/dist/index.d.ts +30 -2
  57. package/dist/index.js +8861 -3938
  58. package/dist/index.js.map +1 -1
  59. package/dist/tailwind.css +96 -96
  60. package/dist/tokens.css +489 -489
  61. package/dist/utils/dinheiro.d.ts +49 -0
  62. package/dist/utils/entidade.d.ts +3 -2
  63. package/dist/utils/periodo.d.ts +36 -0
  64. package/dist/utils/senha.d.ts +2 -1
  65. package/dist/utils/tempo.d.ts +32 -0
  66. package/dist/utils/texto.d.ts +12 -5
  67. package/dist/utils/tiposCampo.d.ts +3 -1
  68. package/dist/utils/validadores.d.ts +4 -2
  69. package/package.json +93 -91
package/README.md CHANGED
@@ -1,638 +1,968 @@
1
- # @celke/kole-react
2
-
3
- Biblioteca de componentes **React** para sistemas administrativos. Design tokens
4
- próprios (cores, raios, densidade, tema claro/escuro), CSS puro e sem dependência
5
- de Tailwind.
6
-
7
- ## Instalação
8
-
9
- ```bash
10
- npm i @celke/kole-react
11
- ```
12
-
13
- Requisitos: React 18.2+ ou 19. A fonte Inter já vem dentro do pacote (o
14
- `estilos.css` carrega só os alfabetos que a página usa). O pacote é só ESM, traz
15
- os tipos TypeScript e já vem marcado com `'use client'`, então funciona no App
16
- Router do Next.js.
17
-
18
- ## Uso
19
-
20
- ```tsx
21
- // main.tsx
22
- import { createRoot } from 'react-dom/client';
23
- import '@celke/kole-react/estilos.css';
24
- import { ProvedorUi } from '@celke/kole-react';
25
- import { App } from './App';
26
-
27
- createRoot(document.getElementById('root')!).render(
28
- <ProvedorUi>
29
- <App />
30
- </ProvedorUi>,
31
- );
32
- ```
33
-
34
- `ProvedorUi` liga tema/densidade (lembrados no navegador), toasts (`useToast`) e
35
- confirmação (`useConfirmacao`).
36
-
37
- ### Tema sem piscar ao abrir
38
-
39
- O `ProvedorUi` só aplica o tema depois que o React monta, e quem salvou o tema
40
- escuro vê a página clara piscar por um instante. Para evitar, aplique o tema no
41
- `<head>`, antes do CSS da página pintar:
42
-
43
- ```html
44
- <!-- index.html -->
45
- <head>
46
- <script>(function(){var p={};try{p=JSON.parse(localStorage.getItem("lp-preferencias")||"{}")||{}}catch(e){}var t=p.tema||"sistema";var h=document.documentElement;h.setAttribute("data-theme",{"claro":"light","escuro":"dark","sistema":"system"}[t]||"light");var d={"compacta":"density-compact","confortavel":"density-comfortable","espacosa":"density-cozy"}[p.densidade||"confortavel"];if(d)h.classList.add(d)})();</script>
47
- </head>
48
- ```
49
-
50
- Com Content-Security-Policy sem `'unsafe-inline'` em `script-src`, o script
51
- inline é bloqueado: salve o mesmo conteúdo num arquivo (por exemplo,
52
- `public/tema.js`) e carregue com `<script src="/tema.js"></script>` no
53
- `<head>`, sem `type="module"` nem `defer`, para rodar antes da pintura.
54
-
55
- Esse é o resultado de `scriptTemaInicial()`. Com SSR (Next.js, React Router em
56
- modo framework), gere pela função, com o mesmo `temaInicial`/`densidadeInicial`
57
- passado ao `ProvedorUi`:
58
-
59
- ```tsx
60
- import { scriptTemaInicial } from '@celke/kole-react';
61
-
62
- <head>
63
- <script dangerouslySetInnerHTML={{ __html: scriptTemaInicial({ temaInicial: 'sistema' }) }} />
64
- </head>
65
- ```
66
-
67
- ### Layout com React Router
68
-
69
- ```tsx
70
- import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom';
71
- import { Estrutura } from '@celke/kole-react';
72
-
73
- export function Layout({ sair }: { sair: () => void }) {
74
- const { pathname } = useLocation();
75
- const navigate = useNavigate();
76
- return (
77
- <Estrutura
78
- marca={{ nome: 'Gestor', subtitulo: 'Gestão comercial', logo: '/logo.svg' }}
79
- nav={[
80
- { itens: [{ rotulo: 'Painel', icone: 'house', rota: '/' }] },
81
- {
82
- rotulo: 'Cadastros',
83
- itens: [
84
- { rotulo: 'Pedidos', icone: 'shopping-cart', rota: '/pedidos' },
85
- { rotulo: 'Tabelas', icone: 'table', filhos: [{ rotulo: 'Categorias', rota: '/categorias' }] },
86
- ],
87
- },
88
- ]}
89
- rotaAtual={pathname}
90
- aoNavegar={navigate}
91
- usuario={{ nome: 'Gabrielly Ribeiro', cargo: 'Gerente comercial' }}
92
- menuUsuario={[{ id: 'sair', rotulo: 'Sair', icone: 'log-out', destrutivo: true }]}
93
- aoEscolherMenuUsuario={(id) => id === 'sair' && sair()}
94
- >
95
- <Outlet />
96
- </Estrutura>
97
- );
98
- }
99
- ```
100
-
101
- Para links de verdade do roteador (ctrl+clique abre em nova aba), passe
102
- `renderLink={(p) => <Link to={p.href} className={p.className} onClick={p.onClick} aria-current={p['aria-current']}>{p.children}</Link>}`.
103
-
104
- ### Páginas fora do React no mesmo app (Inertia + Blade)
105
-
106
- Num app Laravel com páginas Inertia e páginas Blade, o `<Link>` do Inertia só
107
- serve para as páginas Inertia. Marque as outras com `recarregar: true`: o link
108
- vira um `<a>` comum (sem `renderLink` nem `aoNavegar`) e abre com recarga
109
- completa. Vale para o menu, a busca do menu e a trilha. Se a marca aponta para
110
- uma página Blade, use `marca={{ …, recarregar: true }}`.
111
-
112
- ```tsx
113
- import { Link, router, usePage } from '@inertiajs/react';
114
-
115
- <Estrutura
116
- nav={[
117
- {
118
- itens: [
119
- { rotulo: 'Painel', icone: 'house', rota: '/painel' }, // Inertia
120
- { rotulo: 'Relatórios', icone: 'file-text', rota: '/relatorios', recarregar: true }, // Blade
121
- ],
122
- },
123
- ]}
124
- rotaAtual={usePage().url}
125
- aoNavegar={(rota) => router.visit(rota)}
126
- renderLink={(p) => <Link href={p.href} className={p.className} onClick={p.onClick} aria-current={p['aria-current']}>{p.children}</Link>}
127
- …
128
- />
129
- ```
130
-
131
- Do Blade para o Inertia não precisa de nada: um link comum abre a página
132
- Inertia. Para as telas Blade com o mesmo layout, veja o pacote Composer
133
- [`celke/kole-laravel`](https://github.com/celkecursos/kole-laravel).
134
-
135
- ## Componentes
136
-
137
- | Grupo | Componentes |
138
- |---|---|
139
- | Layout | `Estrutura`, `Busca`, `CabecalhoPagina`, `BoasVindas`, `TelaLogin`, `Abas`/`Aba`, `Card` |
140
- | Ações | `Botao`, `AcaoLinha`, `Dica` |
141
- | Formulário | `Form` (página ou modal, `entidade`/`modo`), `FormSecao`, `Campo`, `CampoTexto`, `CampoTextarea`, `CampoSenha`, `CampoSelect`, `CampoSelectMultiplo`, `CampoMarcacao`, `Escolha`, `Picklist`, `Filtros` |
142
- | Campos prontos | `CampoCpf`, `CampoCnpj`, `CampoCpfCnpj`, `CampoCep`, `CampoTelefone`, `CampoPlaca`, `CampoRenavam`, `CampoChassi`, `CampoCnh`, `CampoImei`, `CampoEmail`, `CampoData`, `CampoHora`, `CampoDataHora`, `CampoMoeda`, `CampoInteiro`, `CampoUf`, `CampoUfMultiplo`, `CampoTipoPessoa` |
143
- | Busca remota | `CampoBusca`, `CampoCidade`, `useBuscaAssincrona` |
144
- | Arquivos | `CampoAnexo`, `VisualizadorArquivo`, `DialogoArquivo`, `Foto` |
145
- | Dados | `Tabela`, `ListaCards`, `Paginacao`, `Indicador`/`Indicadores`, `Detalhes`, `Status`, `Chip`, `Avatar`, `Placa` |
146
- | Feedback | `Alerta`, `Dialogo` (central ou `lateral`), `useConfirmacao` (+ `.remover()`, `.avisar()`), `useToast`, `Vazio`, `Skeleton`, `Carregando` |
147
- | Conversa | `Conversa`, `Mensagem`, `CampoMensagem`, `Referencias` |
148
- | Site público | `EstruturaSite`, `RodapeSite`, `SecaoSite`, `DestaqueSite`, `CabecalhoSecao`, `CampoPesquisa`, `CardConteudo`, `CardRecurso`, `CardDepoimento`, `CardPlano`, `FaixaNumeros`, `FaixaChamada`, `ListaNumerada`, `FiltroPilulas`, `ConteudoArtigo`, `SumarioPagina`, `Newsletter` |
149
- | Gráficos | `GraficoLinha`, `GraficoBarras`, `GraficoCombinado`, `GraficoDispersao`, `MapaCalor`, `Minigrafico` (e `tendencia` no `Indicador`), `regressaoLinear` |
150
-
151
- ### Conversa (chat)
152
-
153
- `Conversa` é a área rolável (acompanha o fim durante o streaming e para de
154
- puxar se a pessoa rolar para cima); `Mensagem` é a bolha; `CampoMensagem` é o
155
- campo de envio (Enter envia, Shift+Enter quebra a linha); `Referencias` lista
156
- as fontes de uma resposta. A conversa ocupa a altura do contêiner pai, e a
157
- busca da resposta (rede, streaming, Markdown) fica com o app.
158
-
159
- ```tsx
160
- import { useState } from 'react';
161
- import {
162
- CampoMensagem,
163
- Conversa,
164
- Mensagem,
165
- Referencias,
166
- type AutorMensagem,
167
- type EstadoMensagem,
168
- type ItemReferencia,
169
- } from '@celke/kole-react';
170
-
171
- interface ItemChat {
172
- id: string;
173
- autor: AutorMensagem;
174
- estado: EstadoMensagem;
175
- texto: string;
176
- hora: Date;
177
- fontes?: ItemReferencia[]; // { id, titulo, detalhe?, trecho?, href? }
178
- }
179
-
180
- interface ChatProps {
181
- mensagens: ItemChat[];
182
- gerando: boolean;
183
- enviar: (texto: string) => void;
184
- parar: () => void;
185
- }
186
-
187
- export function Chat({ mensagens, gerando, enviar, parar }: ChatProps) {
188
- const [texto, setTexto] = useState('');
189
- return (
190
- <div style={{ height: '32rem' }}>
191
- <Conversa
192
- campo={
193
- <CampoMensagem
194
- valor={texto}
195
- aoMudar={setTexto}
196
- aoEnviar={(t) => {
197
- enviar(t);
198
- setTexto('');
199
- }}
200
- gerando={gerando}
201
- aoParar={parar}
202
- />
203
- }
204
- >
205
- {mensagens.map((m) => (
206
- <Mensagem
207
- key={m.id}
208
- autor={m.autor} // 'usuario' | 'assistente' | 'sistema'
209
- estado={m.estado} // 'enviando' | 'gerando' | 'concluida' | 'erro'
210
- hora={m.hora}
211
- copiavel={m.autor === 'assistente'}
212
- rodape={m.fontes && <Referencias itens={m.fontes} />}
213
- >
214
- {m.texto}
215
- </Mensagem>
216
- ))}
217
- </Conversa>
218
- </div>
219
- );
220
- }
221
- ```
222
-
223
- ### Site público
224
-
225
- Para a parte aberta do sistema (site institucional, loja, portal) há uma moldura
226
- própria, sem o menu lateral: `EstruturaSite`, com barra de aviso opcional,
227
- cabeçalho fixo (marca, menu horizontal, botão de tema e as `acoes`), conteúdo e
228
- `RodapeSite`. No celular, o menu vai para um painel aberto pelo botão Menu. Os
229
- links são `<a href>` de verdade: dentro da `EstruturaSite`, os cartões, as listas e
230
- a `Trilha` navegam pelo `aoNavegar`/`renderLink` do app, e Ctrl+clique ou o botão
231
- do meio abrem em nova aba. A demo tem as páginas modelo de um site genérico (menu
232
- **Site**): Home (faixa escura ou clara), Sobre, Serviços e planos, Blog, Post (sem
233
- e com coluna lateral), Contato e as páginas legais (Termos de uso, Política de Privacidade e Política de
234
- Cookies, com texto de exemplo a ser revisado por um advogado).
235
-
236
- ```tsx
237
- import { Link, Outlet, useLocation } from 'react-router-dom';
238
- import {
239
- Botao,
240
- CabecalhoSecao,
241
- CardConteudo,
242
- CardDepoimento,
243
- CardPlano,
244
- CardRecurso,
245
- DestaqueSite,
246
- EstruturaSite,
247
- FaixaChamada,
248
- FaixaNumeros,
249
- ListaNumerada,
250
- Newsletter,
251
- RodapeSite,
252
- SecaoSite,
253
- } from '@celke/kole-react';
254
-
255
- export function LayoutSite() {
256
- const { pathname } = useLocation();
257
- return (
258
- <EstruturaSite
259
- marca={{ nome: 'Sua Marca', href: '/', logo: '/logo.svg' }}
260
- nav={[
261
- { rotulo: 'Home', rota: '/' },
262
- { rotulo: 'Serviços', rota: '/servicos' },
263
- { rotulo: 'Contato', rota: '/contato' },
264
- ]}
265
- rotaAtual={pathname}
266
- renderLink={(p) => (
267
- <Link to={p.href} className={p.className} onClick={p.onClick} aria-current={p['aria-current']}>
268
- {p.children}
269
- </Link>
270
- )}
271
- aviso={<><b>Novidade:</b> conheça a nova versão.</>}
272
- acoes={<Botao rotulo="Fale conosco" />}
273
- rodape={
274
- <RodapeSite
275
- nome="Sua Marca"
276
- descricao="O que a empresa faz e para quem."
277
- extra={<Newsletter aparencia="linha" rotuloBotao="Assinar" aoInscrever={async (email) => { /* envie ao servidor */ }} />}
278
- colunas={[{ titulo: 'Navegação', links: [{ rotulo: 'Serviços', rota: '/servicos' }] }]}
279
- copyright="© 2026 Sua Marca"
280
- redes={[{ rotulo: 'Instagram', rota: 'https://instagram.com/suamarca' }]}
281
- />
282
- }
283
- >
284
- <Outlet />
285
- </EstruturaSite>
286
- );
287
- }
288
-
289
- export function Home() {
290
- return (
291
- <>
292
- {/* aparencia="claro" centraliza o texto e põe a `midia` embaixo */}
293
- <DestaqueSite selo="Seu slogan" titulo="Uma frase forte sobre o seu negócio." realce="negócio" lateral={<img src="/produto.png" alt="" />}>
294
- <Botao rotulo="Começar agora" severidade="destaque" tamanho="grande" />
295
- <FaixaNumeros aparencia="simples" itens={[{ valor: '+1.200', rotulo: 'clientes' }]} />
296
- </DestaqueSite>
297
- <SecaoSite>
298
- <CabecalhoSecao sobretitulo="Diferenciais" titulo="Por que escolher a Sua Marca" />
299
- <CardRecurso icone="shield" titulo="Seguro e confiável">Garantias e cuidados com o cliente.</CardRecurso>
300
- <ListaNumerada itens={[{ titulo: 'Contato inicial', descricao: 'O cliente conta o que precisa.' }]} />
301
- <FaixaNumeros itens={[{ valor: '98', sufixo: '%', rotulo: 'de satisfação' }]} />
302
- <CardDepoimento texto="O atendimento foi rápido e o resultado veio." nome="Nome do Cliente" cargo="Cargo · Empresa" />
303
- <CardConteudo href="/blog/post" midia={<img src="/post.jpg" alt="" />} titulo="Título do post" />
304
- <CardPlano nome="Profissional" preco="99" periodo="/mês" itens={['Suporte prioritário']} destaque acao={<Botao rotulo="Assinar" bloco />} />
305
- <FaixaChamada titulo="Pronto para começar?" acoes={<Botao rotulo="Solicitar contato" severidade="destaque" />} />
306
- </SecaoSite>
307
- </>
308
- );
309
- }
310
- ```
311
-
312
- `CardConteudo` tem cinco formatos: `vertical` (grade de posts), `destaque`,
313
- `horizontal` (post em destaque), `compacto` e `linha` (listagem com quadro à
314
- direita). `CardRecurso` serve para diferenciais, serviços (`formato="horizontal"`),
315
- valores (`numero`) e canais de contato (`sobretitulo`). `ConteudoArtigo` estiliza o
316
- texto de posts e páginas como termos de uso; HTML vindo de Markdown deve chegar
317
- sanitizado. `SumarioPagina` lista as seções de uma página longa (pelo `id` dos
318
- títulos), marca a seção em leitura e rola até ela sem trocar o endereço, então
319
- funciona também em app com rotas no `#hash`. `Newsletter` confere o e-mail antes de chamar `aoInscrever`. Para
320
- perguntas frequentes, use o `Acordeao` (`icone="mais"`).
321
-
322
- Tokens próprios do site: `--site-largura`, `--r-site`, `--r-site-2`,
323
- `--sombra-site`, `--fs-site-hero`, `--fs-site-titulo`, `--fs-site-secao` e os tons
324
- sobre o marinho (`--site-escuro-*`). A cor de ação é o `--pri` de sempre: para a cor
325
- da marca do cliente, redefina `--pri`, `--pri-ink`, `--pri-bg` e `--pri-bd` (veja
326
- "Tema e tokens").
327
-
328
- ### Gráficos
329
-
330
- Desenhados em SVG próprio, sem biblioteca de gráficos. A largura
331
- acompanha o container (o desenho é refeito no tamanho novo, sem esticar o texto),
332
- os números e as datas saem em pt-BR (`1.234,5`, `jan/21`) e as cores vêm dos
333
- tokens `--graf-*`, então funcionam nos temas claro, escuro e sistema.
334
-
335
- Comum a todos: `titulo` (obrigatório; vira o nome acessível), `descricao` (resumo
336
- para leitor de tela), `altura` (padrão `280` px), `legenda` (padrão `true`; clicar
337
- esconde ou mostra a série), `carregando`, `textoVazio` e `verDados` (padrão
338
- `true`; alterna entre o gráfico e uma `Tabela` com os mesmos valores, o caminho
339
- completo para quem não enxerga o gráfico). Passe o mouse, toque ou foque o
340
- gráfico e use as setas do teclado: uma caixa mostra o valor de cada série. `null`
341
- é "sem dado": falha na linha, barra ausente, célula hachurada, nunca zero.
342
-
343
- **Animação** (`animacao`, padrão `false`): na entrada, as linhas se desenham da
344
- esquerda para a direita, as barras crescem a partir do zero e os pontos e as
345
- células surgem; rótulos e referências chegam depois. É só CSS e roda uma vez,
346
- quando o gráfico aparece: dados que chegam depois (atualização periódica, por
347
- exemplo) não reiniciam a animação. Com "reduzir movimento" ligado no sistema
348
- (`prefers-reduced-motion`), nada se mexe. O `Indicador` também aceita `animacao`
349
- para o minigráfico da `tendencia`. Prefira deixar desligado em painéis abertos
350
- todo dia: a animação ajuda em apresentações, mas atrasa a leitura.
351
-
352
- ```tsx
353
- import {
354
- GraficoBarras,
355
- GraficoCombinado,
356
- GraficoDispersao,
357
- GraficoLinha,
358
- Indicador,
359
- MapaCalor,
360
- regressaoLinear,
361
- } from '@celke/kole-react';
362
-
363
- const reais = (v: number) => v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL', maximumFractionDigits: 0 });
364
-
365
- // séries no tempo: faixa sombreada no eixo X e linha de referência
366
- <GraficoLinha
367
- titulo="Faturamento mensal"
368
- dados={[{ mes: '2025-10', valor: 248000 }, { mes: '2025-11', valor: null }, { mes: '2025-12', valor: 342000 }]}
369
- eixoX={{ chave: 'mes', tipo: 'data' }}
370
- eixoY={{ rotulo: 'Faturamento', formatar: reais }}
371
- series={[{ chave: 'valor', rotulo: 'Faturamento' }]}
372
- area
373
- faixas={[{ de: '2025-11', ate: '2025-12', rotulo: 'Fim de ano' }]}
374
- referencias={[{ valor: 260000, rotulo: 'Média mensal' }]}
375
- />;
376
-
377
- // barras: várias séries ficam agrupadas; `divergente` pinta acima e abaixo de zero;
378
- // `orientacao="horizontal"` serve para rankings (rótulo longo quebra em duas linhas)
379
- <GraficoBarras
380
- titulo="Faturamento acima ou abaixo da meta"
381
- dados={[{ mes: '2026-05', diferenca: -12 }, { mes: '2026-06', diferenca: 8.5 }]}
382
- eixoX={{ chave: 'mes', tipo: 'data' }}
383
- eixoY={{ unidade: 'mil R$' }}
384
- series={[{ chave: 'diferenca', rotulo: 'Diferença para a meta' }]}
385
- divergente
386
- />;
387
-
388
- // dois eixos: a legenda diz o eixo de cada série ("Ticket médio (R$, eixo direito)")
389
- <GraficoCombinado
390
- titulo="Pedidos e ticket médio"
391
- dados={[{ mes: '2026-05', pedidos: 1420, ticket: 198.4 }, { mes: '2026-06', pedidos: 1510, ticket: 203.1 }]}
392
- eixoX={{ chave: 'mes', tipo: 'data' }}
393
- eixoY={{ rotulo: 'Pedidos' }}
394
- eixoYDireito={{ rotulo: 'Ticket médio', unidade: 'R$' }}
395
- barras={[{ chave: 'pedidos', rotulo: 'Pedidos' }]}
396
- linhas={[{ chave: 'ticket', rotulo: 'Ticket médio' }]}
397
- />;
398
-
399
- // um ponto por item; `regressao` desenha a reta e mostra r, r² e n
400
- <GraficoDispersao
401
- titulo="Preço × unidades vendidas"
402
- dados={produtos} // [{ produto: 'Café torrado — 500 g', preco: 18.9, unidades: 610, promocao: false }, …]
403
- eixoX={{ rotulo: 'Preço', unidade: 'R$' }}
404
- eixoY={{ rotulo: 'Unidades vendidas' }}
405
- chaveX="preco"
406
- chaveY="unidades"
407
- chaveRotulo="produto"
408
- chaveDestaque="promocao"
409
- regressao
410
- nota="Estatísticas a mais (como o p-valor) são calculadas pelo app."
411
- />;
412
-
413
- // grade de células (ex.: hora × dia da semana); com muitas linhas, rola com os rótulos das colunas presos no topo
414
- <MapaCalor
415
- titulo="Pedidos por hora e dia da semana"
416
- linhas={['08h', '09h']}
417
- colunas={['seg', 'ter']}
418
- valores={[{ linha: '08h', coluna: 'seg', valor: 72 }, { linha: '08h', coluna: 'ter', valor: null }]}
419
- unidade="pedidos"
420
- />;
421
-
422
- // minigráfico abaixo do valor, na cor do indicador
423
- <Indicador rotulo="Pedidos em junho" valor={1510} cor="var(--graf-3)" tendencia={[1180, 1240, 1320, 1290, 1420, 1510]} />;
424
-
425
- // a mesma conta da dispersão, para usar no app: { inclinacao, intercepto, r, r2, n } ou null
426
- const reta = regressaoLinear([{ x: 4.99, y: 880 }, { x: 18.9, y: 610 }, { x: 39.9, y: 190 }]);
427
- ```
428
-
429
- Tipos do eixo X: `categoria` (texto, uma faixa por item), `data` (`Date`,
430
- timestamp ou texto `aaaa-mm-dd`/`aaaa-mm`; o rótulo se ajusta a dados diários,
431
- mensais ou anuais) e `numero`. Os rótulos do eixo X nunca se sobrepõem: se não
432
- couberem, aparece um a cada N. `eixo.formatar` troca o formato dos números
433
- (eixo, caixa de detalhes e tabela).
434
-
435
- Tokens (definidos nos dois temas; redefina no CSS do app para mudar a paleta):
436
-
437
- | Token | Uso |
438
- |---|---|
439
- | `--graf-1` a `--graf-6` | Cores das séries, nessa ordem (contraste de 3:1 ou mais contra `--surface`) |
440
- | `--graf-pos` / `--graf-neg` | Barras divergentes, acima e abaixo de zero |
441
- | `--graf-seq-1` a `--graf-seq-5` | Escala do mapa de calor, do valor mais baixo ao mais alto |
442
- | `--graf-grade`, `--graf-eixo`, `--graf-texto` | Linhas de grade, eixos e rótulos |
443
- | `--graf-faixa` | Faixa sombreada de destaque |
444
-
445
- **Volume de pontos:** cada gráfico é fluido até cerca de 2.000 pontos. Séries
446
- maiores devem chegar agregadas (por exemplo, os pedidos de anos somados por mês
447
- em vez de um ponto por pedido, ou o mapa de calor em vez de uma linha só).
448
-
449
- Utilitários (lógica pura, também exportados): validadores BR (`isCpf`, `isCnpj`,
450
- `isCnh`, `isImei`, `validar(tipo, valor)`…), `TIPOS_CAMPO` (máscara +
451
- placeholder + regra por tipo), `ESTADOS`/`TIPOS_PESSOA`, por extenso
452
- (`numeroPorExtenso`, `valorMonetarioPorExtenso`, `listaPorExtenso`), texto
453
- (`normalizar`, `nomeProprio`, `nomeCurto`, `iniciais`, `formatCpfCnpj`), datas
454
- pt-BR (`parseDataBr`, `formatDataBr`…), `tituloDeEntidade`/`nomeDeArquivo`,
455
- anexos (`formatBytes`, `tipoDeArquivo`, `iconeArquivo`), senha
456
- (`validarSenha`, `forcaSenha`), picklist, JWT (`decodificarJwt`, `jwtExpirado`…)
457
- e `severidadeCor`. A demo do repositório tem uma página interativa de cada um
458
- (`#/utilitarios`).
459
-
460
- Convenções:
461
-
462
- - Campos são controlados por `valor` + `aoMudar`; aceitam `rotulo`, `dica`,
463
- `erro`, `aviso`, `obrigatorio` e `variante="inline"` (pílula com rótulo dentro,
464
- para barras de filtro).
465
- - Severidades: `ok`, `info`, `warn`, `err`, `neutral` (botão: `pri`, `secundario`,
466
- `ok`, `warn`, `err`).
467
- - Ícones: [Lucide](https://lucide.dev/icons) (SVG, já incluso no pacote). Passe o
468
- nome (`icone="save"`, `icone="trash-2"`) ou o componente
469
- (`import { Rocket } from 'lucide-react'` → `icone={<Rocket />}`). Só os ícones
470
- registrados entram no bundle; para usar outro pelo nome, registre uma vez:
471
- `registrarIcones({ rocket: Rocket })`. Um nome não registrado avisa no console
472
- em desenvolvimento.
473
- - Pendência de campo: o erro aparece ao **sair** do campo depois de digitar, ou
474
- quando o `Form` tenta salvar (todos os campos acusam). Um valor carregado de
475
- fora que não atende ao formato não é recusado: vira aviso âmbar.
476
- - Grade do `FormSecao`: 4 colunas na página, 3 no modal; `className="lp-col-2"`,
477
- `lp-col-3` ou `lp-col-full` alarga o campo.
478
- - Cadastro com vários formulários: passe `menu` (itens com `id`, `rotulo`,
479
- `icone`, `href`) e `menuAtivo` ao `Form` de cada página; o menu lateral liga
480
- os formulários entre si, cada um com o próprio Salvar.
481
-
482
- ## Tema e tokens
483
-
484
- Toda cor/medida vem de variáveis CSS (os tokens). Para mudar a marca, redefina
485
- os tokens no CSS do app (a ordem dos imports não importa, veja [CSS](#css)) — por exemplo
486
- `--navy` (marinho da sidebar), `--pri` (azul de ação) e `--acento-claro` (laranja
487
- do item ativo). O tema escuro tem valores próprios, então redefina lá também:
488
-
489
- ```css
490
- :root {
491
- --pri: #0f766e;
492
- --pri-ink: #115e59;
493
- }
494
- [data-theme='dark'] {
495
- --pri: #2dd4bf;
496
- --pri-ink: #5eead4;
497
- }
498
- ```
499
-
500
- O tema vem do atributo `data-theme` no `<html>`: `light`, `dark` ou `system`
501
- (segue o modo do sistema operacional, inclusive se ele mudar com a página
502
- aberta). No `ProvedorUi`, são os temas `claro`, `escuro` e `sistema`
503
- (`temaInicial`, `definirTema`). Enquanto a pessoa não escolhe, vale o
504
- `temaInicial`, e sem ele o `sistema`. `escuro` no `usePreferencias()` diz o que
505
- está na tela. A densidade usa as classes `density-compact`, `density-comfortable` e
506
- `density-cozy` no `<body>`. O `ProvedorUi` cuida de tudo isso. A lista
507
- completa de tokens, com os valores dos dois temas, está em
508
- `@celke/kole-react/tokens.css`.
509
-
510
- Para usar só os tokens fora do React: `@import '@celke/kole-react/tokens.css';`
511
- e `<html data-theme="system">` (ou `light`/`dark`) para escolher o tema sem JS.
512
-
513
- ## CSS
514
-
515
- Classes CSS usam o prefixo `lp-`, e todo o CSS da biblioteca fica na camada
516
- `@layer kole`. CSS fora de camada sempre vence CSS em camada, então o CSS do
517
- app personaliza o kole sem disputar especificidade nem depender da ordem dos
518
- imports. O lado oposto: uma regra global do app (`a { color: … }`,
519
- `h1 { font-size: … }`) também atinge os componentes, por isso escope as regras
520
- de elemento do app (por exemplo, `.conteudo-artigo h1`).
521
-
522
- ### Com Tailwind 4
523
-
524
- A ordem das camadas precisa ser declarada **antes** de qualquer CSS, então o
525
- CSS do app (com o Tailwind) é importado antes do `estilos.css`:
526
-
527
- ```css
528
- /* app.css — esta linha vem antes do @import do Tailwind */
529
- @layer theme, base, kole, components, utilities;
530
- @import 'tailwindcss';
531
- @import '@celke/kole-react/tailwind.css'; /* opcional: a ponte abaixo */
532
- ```
533
-
534
- ```tsx
535
- // main.tsx
536
- import './app.css';
537
- import '@celke/kole-react/estilos.css';
538
- ```
539
-
540
- Assim o kole vence o preflight do Tailwind (botões e campos mantêm o estilo) e
541
- os utilitários vencem o kole (`<Botao className="mt-4" />` funciona). Se o
542
- `estilos.css` vier primeiro, a camada `kole` fica abaixo do preflight e os
543
- componentes perdem o estilo.
544
-
545
- A ponte `@celke/kole-react/tailwind.css` faz duas coisas:
546
-
547
- - o `dark:` do Tailwind segue o tema do kole (`data-theme`), e não o do sistema
548
- operacional, então uma tela com os dois não fica metade clara e metade escura;
549
- - os tokens viram utilitários com o mesmo nome: `bg-surface`, `text-ink`,
550
- `text-t3`, `border-border`, `bg-pri`, `text-ok-fg`, `bg-err-bg`,
551
- `rounded-card`, `rounded-pill`… Como apontam para os tokens, trocam de tema
552
- junto com o kole. `font-sans` e `font-mono` passam a ser as fontes do kole.
553
- Para o site público: `max-w-site`, `rounded-site`, `rounded-site-2`,
554
- `shadow-site`, `text-site-hero`, `text-site-titulo`, `text-site-secao` e, sobre
555
- o marinho, `bg-navy text-site-escuro-t3`.
556
-
557
- A densidade não chega aos utilitários: `p-4` e `h-10` são fixos, só os
558
- componentes `lp-*` mudam com ela.
559
-
560
- Vale igual para o site e para o sistema: os componentes de site (`EstruturaSite`,
561
- `DestaqueSite`…) estão no mesmo `estilos.css`, na mesma camada `kole`, então
562
- `<SecaoSite className="mt-8" />` funciona e o preflight não desfaz o estilo deles.
563
-
564
- ## Desenvolvimento
565
-
566
- ```bash
567
- npm install
568
- npm run dev # http://localhost:5173 — demo com todos os componentes
569
- npm run typecheck
570
- npm run build # dist/index.js + tipos (.d.ts) + dist/estilos.css + dist/tokens.css + dist/tailwind.css
571
- ```
572
-
573
- A demo usa o mesmo CSS do pacote (na camada `kole`, com o tema `sistema`), e o
574
- `demo/demo.css` fica fora da camada, como o CSS de um app.
575
-
576
- ### Teste visual
577
-
578
- Captura cada página do menu da demo (`demo/nav.ts`) nos temas claro e escuro e
579
- compara pixel a pixel com uma referência. Também confere que o tema `sistema`,
580
- com o sistema operacional no escuro, sai igual ao tema escuro. Página nova no
581
- menu entra no teste sozinha.
582
-
583
- O print muda de um sistema operacional para outro, então a referência fica em
584
- `tests/visual/__referencia__/<sistema>/`. Só a de Linux vai para o git: é a que
585
- o CI usa (`.github/workflows/visual.yml`, na imagem Docker do Playwright), em
586
- todo PR e na `main`.
587
-
588
- **Mudança visual de propósito:** o CI do PR vai falhar apontando as páginas que
589
- mudaram (o relatório com as diferenças fica nos artefatos do workflow). Confira
590
- e grave a referência nova: Actions → Teste visual → Run workflow, na branch do
591
- PR, com "atualizar" marcado. O workflow faz o commit da referência na branch.
592
-
593
- No Windows ou no macOS, o teste usa uma referência local, fora do git:
594
-
595
- ```bash
596
- npx playwright install chromium # uma vez
597
- npm run visual:referencia # ANTES de mexer no CSS: grava a referência
598
- npm run visual # depois: aponta cada página que mudou
599
- npx playwright show-report # diferenças lado a lado
600
- ```
601
-
602
- Para comparar com uma versão já publicada, sirva a demo dela noutra porta (por
603
- exemplo, num `git worktree` da tag) e gere a referência com
604
- `KOLE_URL=http://localhost:5181 npm run visual:referencia`.
605
-
606
- Para testar uma versão que ainda não está no npm, instale direto do GitHub
607
- (o `prepare` gera o `dist/` na instalação) ou de um pacote local:
608
-
609
- ```bash
610
- npm i github:celkecursos/kole-react
611
- # ou: gere o .tgz na pasta da biblioteca e instale no app
612
- npm pack # → celke-kole-react-<versão>.tgz
613
- npm i ../kole-react/celke-kole-react-<versão>.tgz
614
- ```
615
-
616
- Evite `npm i ../kole-react` (pasta): o npm cria um link e o bundler
617
- pega o React do `node_modules` da biblioteca — dois Reacts no app quebram os hooks
618
- (_Invalid hook call_). Se precisar do link durante o desenvolvimento, use
619
- `resolve: { dedupe: ['react', 'react-dom'] }` no `vite.config` do app.
620
-
621
- Publicar uma versão nova: `npm version patch` (só correção) ou `npm version minor`
622
- (muda a aparência ou a API — antes da 1.0, o `^0.x` dos apps não pega a minor
623
- sozinho), registre no [CHANGELOG](CHANGELOG.md) e confira o conteúdo com
624
- `npm pack --dry-run`. Depois do merge na `main`, publique pelo GitHub: Actions →
625
- Publicar → Run workflow (`.github/workflows/publish.yml`). O npm confia nesse
626
- workflow pela publicação confiável (Trusted Publisher, sem token) e o pacote
627
- sai com o atestado de procedência. O workflow só deixa a versão em espera
628
- (`npm stage publish`): ela vai ao ar depois que um mantenedor aprova com o
629
- segundo fator, com `npm stage list @celke/kole-react` e `npm stage approve <id>`
630
- (ou pelo site do npm). O workflow recusa versão que já está no npm.
631
-
632
- ## Autor
633
-
634
- Criado por **Cesar Szpak**.
635
-
636
- ## Licença
637
-
638
- MIT © 2026 Celke — veja o arquivo [LICENSE](https://github.com/celkecursos/kole-react/blob/main/LICENSE).
1
+ # @celke/kole-react
2
+
3
+ Biblioteca de componentes **React** para sistemas administrativos. Design tokens
4
+ próprios (cores, raios, densidade, tema claro/escuro), CSS puro e sem dependência
5
+ de Tailwind.
6
+
7
+ ## Instalação
8
+
9
+ ```bash
10
+ npm i @celke/kole-react
11
+ ```
12
+
13
+ Requisitos: React 18.2+ ou 19. A fonte Inter já vem dentro do pacote (o
14
+ `estilos.css` carrega só os alfabetos que a página usa). O pacote é só ESM, traz
15
+ os tipos TypeScript e já vem marcado com `'use client'`, então funciona no App
16
+ Router do Next.js.
17
+
18
+ ## Uso
19
+
20
+ ```tsx
21
+ // main.tsx
22
+ import { createRoot } from 'react-dom/client';
23
+ import '@celke/kole-react/estilos.css';
24
+ import { ProvedorUi } from '@celke/kole-react';
25
+ import { App } from './App';
26
+
27
+ createRoot(document.getElementById('root')!).render(
28
+ <ProvedorUi>
29
+ <App />
30
+ </ProvedorUi>,
31
+ );
32
+ ```
33
+
34
+ `ProvedorUi` liga tema/densidade (lembrados no navegador), toasts (`useToast`) e
35
+ confirmação (`useConfirmacao`).
36
+
37
+ ### Idioma
38
+
39
+ Os textos da própria biblioteca saem em português do Brasil (padrão), inglês ou
40
+ espanhol. Ligue o seletor na topbar com `idiomas` e sincronize o i18n do seu
41
+ sistema em `aoMudarIdioma` (chamado ao montar e a cada troca). A biblioteca não
42
+ traduz os textos do sistema: ela só informa o idioma escolhido.
43
+
44
+ ```tsx
45
+ <ProvedorUi idiomaInicial={usuario.locale} aoMudarIdioma={(idioma) => i18n.changeLanguage(idioma)}>
46
+ <Estrutura idiomas={['pt-BR', 'en', 'es']} {...props}>…</Estrutura>
47
+ </ProvedorUi>
48
+ ```
49
+
50
+ Prioridade dos textos: prop passada > dicionário do idioma > pt-BR. Para trocar
51
+ um texto ou acrescentar um idioma, passe `textos` ao provedor
52
+ (`{ 'pt-BR': { tabela: { rotulo: 'Lista' } } }`). Regras brasileiras (máscaras e
53
+ validação de CPF, CNPJ, CEP e telefone) e a moeda não mudam com o idioma; só a
54
+ formatação e as mensagens. A demo traz a página Idioma com os exemplos de
55
+ react-i18next e Laravel.
56
+
57
+ ### Tema sem piscar ao abrir
58
+
59
+ O `ProvedorUi` só aplica o tema depois que o React monta, e quem salvou o tema
60
+ escuro vê a página clara piscar por um instante. Para evitar, aplique o tema no
61
+ `<head>`, antes do CSS da página pintar:
62
+
63
+ ```html
64
+ <!-- index.html -->
65
+ <head>
66
+ <script>(function(){var p={};try{p=JSON.parse(localStorage.getItem("lp-preferencias")||"{}")||{}}catch(e){}var t=p.tema||"sistema";var h=document.documentElement;h.setAttribute("data-theme",{"claro":"light","escuro":"dark","sistema":"system"}[t]||"light");var l=p.idioma||"";if(l)h.lang=l;var d={"compacta":"density-compact","confortavel":"density-comfortable","espacosa":"density-cozy"}[p.densidade||"confortavel"];if(d)h.classList.add(d)})();</script>
67
+ </head>
68
+ ```
69
+
70
+ Com Content-Security-Policy sem `'unsafe-inline'` em `script-src`, o script
71
+ inline é bloqueado: salve o mesmo conteúdo num arquivo (por exemplo,
72
+ `public/tema.js`) e carregue com `<script src="/tema.js"></script>` no
73
+ `<head>`, sem `type="module"` nem `defer`, para rodar antes da pintura.
74
+
75
+ Esse é o resultado de `scriptTemaInicial()`. Com SSR (Next.js, React Router em
76
+ modo framework), gere pela função, com o mesmo `temaInicial`/`densidadeInicial`
77
+ passado ao `ProvedorUi`:
78
+
79
+ ```tsx
80
+ import { scriptTemaInicial } from '@celke/kole-react';
81
+
82
+ <head>
83
+ <script dangerouslySetInnerHTML={{ __html: scriptTemaInicial({ temaInicial: 'sistema' }) }} />
84
+ </head>
85
+ ```
86
+
87
+ ### Layout com React Router
88
+
89
+ ```tsx
90
+ import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom';
91
+ import { Estrutura } from '@celke/kole-react';
92
+
93
+ export function Layout({ sair }: { sair: () => void }) {
94
+ const { pathname } = useLocation();
95
+ const navigate = useNavigate();
96
+ return (
97
+ <Estrutura
98
+ marca={{ nome: 'Gestor', subtitulo: 'Gestão comercial', logo: '/logo.svg' }}
99
+ nav={[
100
+ { itens: [{ rotulo: 'Painel', icone: 'house', rota: '/' }] },
101
+ {
102
+ rotulo: 'Cadastros',
103
+ itens: [
104
+ { rotulo: 'Pedidos', icone: 'shopping-cart', rota: '/pedidos' },
105
+ { rotulo: 'Tabelas', icone: 'table', filhos: [{ rotulo: 'Categorias', rota: '/categorias' }] },
106
+ ],
107
+ },
108
+ ]}
109
+ rotaAtual={pathname}
110
+ aoNavegar={navigate}
111
+ usuario={{ nome: 'Gabrielly Ribeiro', cargo: 'Gerente comercial' }}
112
+ menuUsuario={[{ id: 'sair', rotulo: 'Sair', icone: 'log-out', destrutivo: true }]}
113
+ aoEscolherMenuUsuario={(id) => id === 'sair' && sair()}
114
+ >
115
+ <Outlet />
116
+ </Estrutura>
117
+ );
118
+ }
119
+ ```
120
+
121
+ Para links de verdade do roteador (ctrl+clique abre em nova aba), passe
122
+ `renderLink={(p) => <Link to={p.href} className={p.className} onClick={p.onClick} aria-current={p['aria-current']}>{p.children}</Link>}`.
123
+
124
+ ### Páginas fora do React no mesmo app (Inertia + Blade)
125
+
126
+ Num app Laravel com páginas Inertia e páginas Blade, o `<Link>` do Inertia só
127
+ serve para as páginas Inertia. Marque as outras com `recarregar: true`: o link
128
+ vira um `<a>` comum (sem `renderLink` nem `aoNavegar`) e abre com recarga
129
+ completa. Vale para o menu, a busca do menu e a trilha. Se a marca aponta para
130
+ uma página Blade, use `marca={{ …, recarregar: true }}`.
131
+
132
+ ```tsx
133
+ import { Link, router, usePage } from '@inertiajs/react';
134
+
135
+ <Estrutura
136
+ nav={[
137
+ {
138
+ itens: [
139
+ { rotulo: 'Painel', icone: 'house', rota: '/painel' }, // Inertia
140
+ { rotulo: 'Relatórios', icone: 'file-text', rota: '/relatorios', recarregar: true }, // Blade
141
+ ],
142
+ },
143
+ ]}
144
+ rotaAtual={usePage().url}
145
+ aoNavegar={(rota) => router.visit(rota)}
146
+ renderLink={(p) => <Link href={p.href} className={p.className} onClick={p.onClick} aria-current={p['aria-current']}>{p.children}</Link>}
147
+ …
148
+ />
149
+ ```
150
+
151
+ Do Blade para o Inertia não precisa de nada: um link comum abre a página
152
+ Inertia. Para as telas Blade com o mesmo layout, veja o pacote Composer
153
+ [`celke/kole-laravel`](https://github.com/celkecursos/kole-laravel).
154
+
155
+ ## Componentes
156
+
157
+ | Grupo | Componentes |
158
+ |---|---|
159
+ | Layout | `Estrutura`, `Busca`, `CabecalhoPagina`, `BoasVindas`, `TelaLogin`, `Abas`/`Aba`, `Card` |
160
+ | Ações | `Botao`, `AcaoLinha`, `Dica` |
161
+ | Formulário | `Form` (página ou modal, `entidade`/`modo`), `FormSecao`, `Campo`, `CampoTexto`, `CampoTextarea`, `CampoSenha`, `CampoSelect`, `CampoSelectMultiplo`, `CampoMarcacao`, `Escolha`, `Picklist`, `Filtros` |
162
+ | Campos prontos | `CampoCpf`, `CampoCnpj`, `CampoCpfCnpj`, `CampoCep`, `CampoTelefone`, `CampoPlaca`, `CampoRenavam`, `CampoChassi`, `CampoCnh`, `CampoImei`, `CampoEmail`, `CampoData`, `CampoHora`, `CampoDataHora`, `CampoMoeda`, `CampoInteiro`, `CampoUf`, `CampoUfMultiplo`, `CampoTipoPessoa` |
163
+ | Busca remota | `CampoBusca`, `CampoCidade`, `useBuscaAssincrona` |
164
+ | Arquivos | `CampoAnexo`, `VisualizadorArquivo`, `DialogoArquivo`, `Foto` |
165
+ | Dados | `Tabela`, `ListaCards`, `Paginacao`, `Indicador`/`Indicadores`, `Detalhes`, `Status`, `Chip`, `Avatar`, `GrupoAvatares`, `Placa` |
166
+ | Feedback | `Alerta`, `Dialogo` (central ou `lateral`), `useConfirmacao` (+ `.remover()`, `.avisar()`), `useToast`, `Vazio`, `Skeleton`, `Carregando` |
167
+ | Conversa | `Conversa`, `Mensagem`, `CampoMensagem`, `Referencias`, `ListaConversas`, `InicioConversa`, `BlocoCodigo`, `ChatFlutuante` |
168
+ | Site público | `EstruturaSite`, `RodapeSite`, `SecaoSite`, `DestaqueSite`, `CabecalhoSecao`, `CampoPesquisa`, `CardConteudo`, `CardRecurso`, `CardDepoimento`, `CardPlano`, `FaixaNumeros`, `FaixaChamada`, `ListaNumerada`, `FiltroPilulas`, `ConteudoArtigo`, `SumarioPagina`, `Newsletter` |
169
+ | Calendário | `Calendario` (visões mês, semana, dia e lista, com arrastar), `MiniCalendario`, `repetirEvento` |
170
+ | Kanban e tarefas | `Kanban`, `CartaoKanban`, `ListaTarefas`, `QuadroArrastavel`, `SeloPrioridade`, `SeloPrazo`, `Etiqueta`, `tempoRelativo`, `situacaoPrazo` |
171
+ | Gráficos | `GraficoLinha`, `GraficoBarras`, `GraficoCombinado`, `GraficoDispersao`, `GraficoRosca`, `MapaCalor`, `Minigrafico` (e `tendencia` no `Indicador`), `regressaoLinear` |
172
+ | Idioma | `SeletorIdioma`, `Bandeira`, `useTextos`, `useIdioma`, `textosPtBR`, `textosEn`, `textosEs`, `interpolar`, `pluralizar` (e `idiomas` na `Estrutura`) |
173
+ | Finanças | `Valor`, `ProvedorValores`, `BotaoOcultarValores`, `NavegadorPeriodo`, `SeletorContexto`, `formatarMoeda`, `formatarPercentual`, `dividirEmParcelas`, `periodoDe`, `noPeriodo` |
174
+
175
+ ### Conversa (chat)
176
+
177
+ `Conversa` é a área rolável (acompanha o fim durante o streaming e para de
178
+ puxar se a pessoa rolar para cima); `Mensagem` é a bolha; `CampoMensagem` é o
179
+ campo de envio (Enter envia, Shift+Enter quebra a linha); `Referencias` lista
180
+ as fontes de uma resposta. A conversa ocupa a altura do contêiner pai, e a
181
+ busca da resposta (rede, streaming, Markdown) fica com o app.
182
+
183
+ ```tsx
184
+ import { useState } from 'react';
185
+ import {
186
+ CampoMensagem,
187
+ Conversa,
188
+ Mensagem,
189
+ Referencias,
190
+ type AutorMensagem,
191
+ type EstadoMensagem,
192
+ type ItemReferencia,
193
+ } from '@celke/kole-react';
194
+
195
+ interface ItemChat {
196
+ id: string;
197
+ autor: AutorMensagem;
198
+ estado: EstadoMensagem;
199
+ texto: string;
200
+ hora: Date;
201
+ fontes?: ItemReferencia[]; // { id, titulo, detalhe?, trecho?, href? }
202
+ }
203
+
204
+ interface ChatProps {
205
+ mensagens: ItemChat[];
206
+ gerando: boolean;
207
+ enviar: (texto: string) => void;
208
+ parar: () => void;
209
+ }
210
+
211
+ export function Chat({ mensagens, gerando, enviar, parar }: ChatProps) {
212
+ const [texto, setTexto] = useState('');
213
+ return (
214
+ <div style={{ height: '32rem' }}>
215
+ <Conversa
216
+ campo={
217
+ <CampoMensagem
218
+ valor={texto}
219
+ aoMudar={setTexto}
220
+ aoEnviar={(t) => {
221
+ enviar(t);
222
+ setTexto('');
223
+ }}
224
+ gerando={gerando}
225
+ aoParar={parar}
226
+ />
227
+ }
228
+ >
229
+ {mensagens.map((m) => (
230
+ <Mensagem
231
+ key={m.id}
232
+ autor={m.autor} // 'usuario' | 'assistente' | 'sistema'
233
+ estado={m.estado} // 'enviando' | 'gerando' | 'concluida' | 'erro'
234
+ hora={m.hora}
235
+ copiavel={m.autor === 'assistente'}
236
+ rodape={m.fontes && <Referencias itens={m.fontes} />}
237
+ >
238
+ {m.texto}
239
+ </Mensagem>
240
+ ))}
241
+ </Conversa>
242
+ </div>
243
+ );
244
+ }
245
+ ```
246
+
247
+ #### Assistente de IA
248
+
249
+ A demo tem a página modelo **Assistente** (Advanced UI): histórico ao lado,
250
+ tela inicial com sugestões, anexos, ferramentas, seletor de modo e respostas com
251
+ tabela e código. As peças:
252
+
253
+ - `ListaConversas`: histórico agrupado por data (Fixadas, Hoje, Ontem, Últimos 7
254
+ dias, Últimos 30 dias e os meses), busca sem acento, renomear no próprio item
255
+ (Enter salva, Esc cancela), fixar e excluir. Ocupa a altura do pai.
256
+ - `InicioConversa`: saudação e sugestões, no `vazio` da `Conversa`. Em painel
257
+ estreito, `aparencia="pilulas"`.
258
+ - `CampoMensagem` com `anexos` (chips com `progresso` e `erro`), `aoRemoverAnexo`
259
+ e `aoAdicionarArquivos` (arrastar para o campo ou colar). Com anexos, dá para
260
+ enviar sem texto; enquanto um anexo sobe, o Enviar espera.
261
+ - `BlocoCodigo`: código com a linguagem e o botão de copiar. A `Mensagem` já
262
+ estiliza o Markdown renderizado pelo app (listas, tabelas, citações).
263
+
264
+ ```tsx
265
+ import ReactMarkdown from 'react-markdown';
266
+ import { BlocoCodigo, InicioConversa, ListaConversas, type AnexoMensagem } from '@celke/kole-react';
267
+
268
+ <ListaConversas
269
+ itens={conversas} // { id, titulo, data, fixada? }
270
+ ativa={ativa}
271
+ aoEscolher={setAtiva}
272
+ aoNova={() => setAtiva(null)}
273
+ aoRenomear={(id, titulo) => api.renomear(id, titulo)}
274
+ aoExcluir={(id) => api.excluir(id)}
275
+ />;
276
+
277
+ <Conversa
278
+ vazio={<InicioConversa titulo="Como posso ajudar?" sugestoes={[{ titulo: 'Resumir as vendas do mês', icone: 'chart-line' }]} aoEscolher={(s) => enviar(s.titulo)} />}
279
+ campo={<CampoMensagem valor={texto} aoMudar={setTexto} aoEnviar={enviar} anexos={anexos} aoRemoverAnexo={remover} aoAdicionarArquivos={subir} />}
280
+ >
281
+ {mensagens.map((m) => (
282
+ <Mensagem key={m.id} autor={m.autor} estado={m.estado}>
283
+ <ReactMarkdown
284
+ components={{
285
+ code: ({ className, children }) =>
286
+ className ? <BlocoCodigo linguagem={className.replace('language-', '')} codigo={String(children).trimEnd()} /> : <code>{children}</code>,
287
+ }}
288
+ >
289
+ {m.texto}
290
+ </ReactMarkdown>
291
+ </Mensagem>
292
+ ))}
293
+ </Conversa>;
294
+ ```
295
+
296
+ A chamada à API (streaming), o histórico salvo, o envio dos anexos e o
297
+ Markdown ficam com o app.
298
+
299
+ #### Chat flutuante (site)
300
+
301
+ `ChatFlutuante` é o atendimento de site: botão fixo no canto ("Fale com a
302
+ gente", com `naoLidas`) que abre um painel com a conversa. Esc fecha e o foco
303
+ volta ao botão; no celular, o painel ocupa a tela. As páginas modelo do site da
304
+ demo já trazem um, com as perguntas frequentes como sugestões.
305
+
306
+ ```tsx
307
+ <ChatFlutuante titulo="Fale com a gente" subtitulo="Respondemos em poucos minutos" naoLidas={1}>
308
+ <Conversa
309
+ vazio={<InicioConversa aparencia="pilulas" titulo="Olá! Como podemos ajudar?" sugestoes={faq} aoEscolher={(s) => enviar(s.titulo)} />}
310
+ campo={<CampoMensagem valor={texto} aoMudar={setTexto} aoEnviar={enviar} />}
311
+ >
312
+ {mensagens}
313
+ </Conversa>
314
+ </ChatFlutuante>
315
+ ```
316
+
317
+ ### Site público
318
+
319
+ Para a parte aberta do sistema (site institucional, loja, portal) há uma moldura
320
+ própria, sem o menu lateral: `EstruturaSite`, com barra de aviso opcional,
321
+ cabeçalho fixo (marca, menu horizontal, botão de tema e as `acoes`), conteúdo e
322
+ `RodapeSite`. No celular, o menu vai para um painel aberto pelo botão Menu. Os
323
+ links são `<a href>` de verdade: dentro da `EstruturaSite`, os cartões, as listas e
324
+ a `Trilha` navegam pelo `aoNavegar`/`renderLink` do app, e Ctrl+clique ou o botão
325
+ do meio abrem em nova aba. A demo tem as páginas modelo de um site genérico (menu
326
+ **Site**): Home (faixa escura ou clara), Sobre, Serviços e planos, Blog, Post (sem
327
+ e com coluna lateral), Contato e as páginas legais (Termos de uso, Política de Privacidade e Política de
328
+ Cookies, com texto de exemplo a ser revisado por um advogado).
329
+
330
+ ```tsx
331
+ import { Link, Outlet, useLocation } from 'react-router-dom';
332
+ import {
333
+ Botao,
334
+ CabecalhoSecao,
335
+ CardConteudo,
336
+ CardDepoimento,
337
+ CardPlano,
338
+ CardRecurso,
339
+ DestaqueSite,
340
+ EstruturaSite,
341
+ FaixaChamada,
342
+ FaixaNumeros,
343
+ ListaNumerada,
344
+ Newsletter,
345
+ RodapeSite,
346
+ SecaoSite,
347
+ } from '@celke/kole-react';
348
+
349
+ export function LayoutSite() {
350
+ const { pathname } = useLocation();
351
+ return (
352
+ <EstruturaSite
353
+ marca={{ nome: 'Sua Marca', href: '/', logo: '/logo.svg' }}
354
+ nav={[
355
+ { rotulo: 'Home', rota: '/' },
356
+ { rotulo: 'Serviços', rota: '/servicos' },
357
+ { rotulo: 'Contato', rota: '/contato' },
358
+ ]}
359
+ rotaAtual={pathname}
360
+ renderLink={(p) => (
361
+ <Link to={p.href} className={p.className} onClick={p.onClick} aria-current={p['aria-current']}>
362
+ {p.children}
363
+ </Link>
364
+ )}
365
+ aviso={<><b>Novidade:</b> conheça a nova versão.</>}
366
+ acoes={<Botao rotulo="Fale conosco" />}
367
+ rodape={
368
+ <RodapeSite
369
+ nome="Sua Marca"
370
+ descricao="O que a empresa faz e para quem."
371
+ extra={<Newsletter aparencia="linha" rotuloBotao="Assinar" aoInscrever={async (email) => { /* envie ao servidor */ }} />}
372
+ colunas={[{ titulo: 'Navegação', links: [{ rotulo: 'Serviços', rota: '/servicos' }] }]}
373
+ copyright="© 2026 Sua Marca"
374
+ redes={[{ rotulo: 'Instagram', rota: 'https://instagram.com/suamarca' }]}
375
+ />
376
+ }
377
+ >
378
+ <Outlet />
379
+ </EstruturaSite>
380
+ );
381
+ }
382
+
383
+ export function Home() {
384
+ return (
385
+ <>
386
+ {/* aparencia="claro" centraliza o texto e põe a `midia` embaixo */}
387
+ <DestaqueSite selo="Seu slogan" titulo="Uma frase forte sobre o seu negócio." realce="negócio" lateral={<img src="/produto.png" alt="" />}>
388
+ <Botao rotulo="Começar agora" severidade="destaque" tamanho="grande" />
389
+ <FaixaNumeros aparencia="simples" itens={[{ valor: '+1.200', rotulo: 'clientes' }]} />
390
+ </DestaqueSite>
391
+ <SecaoSite>
392
+ <CabecalhoSecao sobretitulo="Diferenciais" titulo="Por que escolher a Sua Marca" />
393
+ <CardRecurso icone="shield" titulo="Seguro e confiável">Garantias e cuidados com o cliente.</CardRecurso>
394
+ <ListaNumerada itens={[{ titulo: 'Contato inicial', descricao: 'O cliente conta o que precisa.' }]} />
395
+ <FaixaNumeros itens={[{ valor: '98', sufixo: '%', rotulo: 'de satisfação' }]} />
396
+ <CardDepoimento texto="O atendimento foi rápido e o resultado veio." nome="Nome do Cliente" cargo="Cargo · Empresa" />
397
+ <CardConteudo href="/blog/post" midia={<img src="/post.jpg" alt="" />} titulo="Título do post" />
398
+ <CardPlano nome="Profissional" preco="99" periodo="/mês" itens={['Suporte prioritário']} destaque acao={<Botao rotulo="Assinar" bloco />} />
399
+ <FaixaChamada titulo="Pronto para começar?" acoes={<Botao rotulo="Solicitar contato" severidade="destaque" />} />
400
+ </SecaoSite>
401
+ </>
402
+ );
403
+ }
404
+ ```
405
+
406
+ `CardConteudo` tem cinco formatos: `vertical` (grade de posts), `destaque`,
407
+ `horizontal` (post em destaque), `compacto` e `linha` (listagem com quadro à
408
+ direita). `CardRecurso` serve para diferenciais, serviços (`formato="horizontal"`),
409
+ valores (`numero`) e canais de contato (`sobretitulo`). `ConteudoArtigo` estiliza o
410
+ texto de posts e páginas como termos de uso; HTML vindo de Markdown deve chegar
411
+ sanitizado. `SumarioPagina` lista as seções de uma página longa (pelo `id` dos
412
+ títulos), marca a seção em leitura e rola até ela sem trocar o endereço, então
413
+ funciona também em app com rotas no `#hash`. `Newsletter` confere o e-mail antes de chamar `aoInscrever`. Para
414
+ perguntas frequentes, use o `Acordeao` (`icone="mais"`).
415
+
416
+ Tokens próprios do site: `--site-largura`, `--r-site`, `--r-site-2`,
417
+ `--sombra-site`, `--fs-site-hero`, `--fs-site-titulo`, `--fs-site-secao` e os tons
418
+ sobre o marinho (`--site-escuro-*`). A cor de ação é o `--pri` de sempre: para a cor
419
+ da marca do cliente, redefina `--pri`, `--pri-ink`, `--pri-bg` e `--pri-bd` (veja
420
+ "Tema e tokens").
421
+
422
+ ### Calendário
423
+
424
+ `Calendario` mostra eventos na visão **mês** (grade de 6 semanas, hoje em
425
+ destaque, até `maxPorDia` eventos por dia e "+N mais", que abre a lista do dia),
426
+ nas visões **semana** e **dia** (faixa de horários de `horaInicio` a `horaFim`,
427
+ eventos posicionados pela hora, os que se sobrepõem lado a lado, faixa de dia
428
+ inteiro no topo e a linha vermelha do "agora") e na visão **lista** (eventos
429
+ agrupados por dia, a melhor no celular). O app guarda
430
+ e busca os eventos: o componente avisa o período visível em `aoMudarPeriodo` e os
431
+ cliques em `aoClicarDia` (ex.: criar evento) e `aoClicarEvento`.
432
+
433
+ ```tsx
434
+ import { Calendario, type EventoCalendario } from '@celke/kole-react';
435
+
436
+ const eventos: EventoCalendario[] = [
437
+ { id: 1, titulo: 'Reunião de vendas', inicio: new Date(2026, 10, 9, 8, 30), fim: new Date(2026, 10, 9, 9, 30), cor: 'pri', categoria: 'Reunião' },
438
+ { id: 2, titulo: 'Promoção de Pix', inicio: new Date(2026, 10, 12), fim: new Date(2026, 10, 18), diaInteiro: true, cor: 'graf-2' },
439
+ ];
440
+
441
+ <Calendario
442
+ eventos={eventos}
443
+ inicioSemana={1} // 0 = domingo (padrão), 1 = segunda
444
+ aoMudarPeriodo={({ inicio, fim }) => {
445
+ /* buscar os eventos de [inicio, fim) */
446
+ }}
447
+ aoClicarDia={(dia) => {
448
+ /* abrir o formulário de novo evento */
449
+ }}
450
+ aoClicarEvento={(evento) => {
451
+ /* abrir o evento */
452
+ }}
453
+ horaInicio={7} // semana e dia: faixa de horários (padrão 7h às 21h)
454
+ horaFim={21}
455
+ aoClicarHorario={(dataHora) => {
456
+ /* semana e dia: clique num horário vazio, arredondado para a meia hora */
457
+ }}
458
+ />;
459
+ ```
460
+
461
+ Cores do evento pelos tokens: `pri`, `ok`, `warn`, `err`, `info`, `destaque` e
462
+ `graf-1` a `graf-6`. Teclado na visão mês: setas andam pelos dias, Home/End vão
463
+ ao começo e ao fim da semana, Page Up/Down trocam de mês e Enter equivale ao
464
+ clique no dia. Na semana e no dia, um cursor de 30 minutos: ↑/↓ mudam o horário,
465
+ ←/→ o dia (passando para a semana ou o dia vizinho), Home/End vão ao primeiro e
466
+ ao último horário e Enter chama `aoClicarHorario` (sem ele, abre o evento daquele
467
+ horário). O leitor de tela ouve o dia, a hora e os eventos do horário. `visoes` escolhe quais visões aparecem na barra (padrão: as quatro).
468
+
469
+ #### Arrastar e esticar
470
+
471
+ Com `aoAlterarEvento`, o evento é arrastado para outro dia (mês e faixa de dia
472
+ inteiro) ou horário (semana e dia, de 15 em 15 minutos), e a borda de baixo do
473
+ bloco estica a duração. No toque, segure o evento antes de arrastar; Esc cancela.
474
+ Pelo teclado, Alt+setas movem (←/→ um dia; ↑/↓ uma semana no mês ou 15 minutos na
475
+ semana e no dia) e Alt+Shift+↑/↓ esticam. O calendário só avisa: o app grava e
476
+ devolve a lista nova em `eventos`. `fixo: true` trava um evento.
477
+
478
+ ```tsx
479
+ <Calendario
480
+ eventos={eventos}
481
+ aoAlterarEvento={(evento, { inicio, fim, tipo }) => {
482
+ // tipo: 'mover' (mesma duração) ou 'esticar' (só o fim mudou)
483
+ setEventos((l) => l.map((e) => (e.id === evento.id ? { ...e, inicio, fim } : e)));
484
+ }}
485
+ />
486
+ ```
487
+
488
+ #### Cadastrar: intervalo e rascunho
489
+
490
+ Na semana e no dia, `aoSelecionarHorario` recebe `{ inicio, fim }`: arrastar sobre
491
+ horários vazios escolhe o intervalo (de 15 em 15 minutos) e um clique escolhe a
492
+ meia hora. No cursor do teclado, Shift+↑/↓ aumentam o intervalo e Enter
493
+ confirma. Com ele, o clique na faixa de horários não chama o `aoClicarHorario`.
494
+ Abra o formulário do app e passe os dados em `rascunho`: o evento aparece
495
+ tracejado, em qualquer visão, enquanto o formulário está aberto.
496
+
497
+ ```tsx
498
+ const [novo, setNovo] = useState<{ inicio: Date; fim: Date; titulo: string } | null>(null);
499
+
500
+ <Calendario
501
+ eventos={eventos}
502
+ aoSelecionarHorario={({ inicio, fim }) => setNovo({ inicio, fim, titulo: '' })}
503
+ rascunho={novo} // { inicio, fim?, titulo?, cor?, diaInteiro? }
504
+ />;
505
+ ```
506
+
507
+ O formulário fica no app (cada sistema tem seus campos). A documentação da demo
508
+ traz um pronto para copiar, com título, categoria, dia inteiro, data, horários,
509
+ "Repete" e excluir só um dia ou a série toda.
510
+
511
+ #### Eventos que se repetem
512
+
513
+ A série fica no app (o evento base e a regra), e `repetirEvento` calcula as
514
+ ocorrências do período visível. Cada ocorrência ganha `id` `"<id>@aaaa-mm-dd"`,
515
+ `repete: true` (ícone de repetição) e `serie` com o id da base. Mensal no dia 31
516
+ pula os meses sem esse dia.
517
+
518
+ ```tsx
519
+ import { repetirEvento, type PeriodoCalendario } from '@celke/kole-react';
520
+
521
+ const [periodo, setPeriodo] = useState<PeriodoCalendario | null>(null);
522
+ const ocorrencias = periodo
523
+ ? repetirEvento(
524
+ { id: 'vendas', titulo: 'Reunião de vendas', inicio: new Date(2026, 10, 2, 9), fim: new Date(2026, 10, 2, 10) },
525
+ { frequencia: 'semanal', diasSemana: [1, 4], ate: new Date(2026, 11, 31), excecoes: [new Date(2026, 10, 19)] },
526
+ periodo,
527
+ )
528
+ : [];
529
+
530
+ <Calendario eventos={[...ocorrencias, ...avulsos]} aoMudarPeriodo={setPeriodo} />;
531
+ ```
532
+
533
+ Regra: `frequencia` (`diaria`, `semanal`, `mensal`, `anual`), `intervalo` (a cada
534
+ N), `diasSemana` (semanal, `0` = domingo), `ate`, `vezes` e `excecoes` (dias que a
535
+ série pula). Para mudar só uma ocorrência arrastada, ponha o dia dela em
536
+ `excecoes` e crie um evento avulso com o horário novo.
537
+
538
+ #### MiniCalendario
539
+
540
+ Mês pequeno para a coluna lateral: escolhe o dia, mostra bolinhas nos dias com
541
+ evento e destaca a semana visível. Ligue o dia escolhido à prop `data` do
542
+ `Calendario`, que leva a agenda até ele.
543
+
544
+ ```tsx
545
+ <MiniCalendario
546
+ valor={dia}
547
+ aoEscolher={setDia}
548
+ marcados={diasComEvento} // Date[]
549
+ destaque={periodo} // { inicio, fim } com fim exclusivo, ex.: a semana do Calendario
550
+ aoMudarMes={(mes) => {
551
+ /* buscar os marcados do mês */
552
+ }}
553
+ />
554
+ <Calendario eventos={eventos} data={dia} aoMudarPeriodo={setPeriodo} />
555
+ ```
556
+ No celular, a semana mostra os eventos como faixas de cor; use dia ou lista para
557
+ ler os títulos.
558
+
559
+ ### Kanban e tarefas
560
+
561
+ `Kanban` é o quadro pronto: colunas com cor, contagem, `limite` de cartões e
562
+ menu (renomear, recolher, excluir), cartões que se arrastam entre colunas (mouse,
563
+ toque e teclado), "Adicionar cartão", "Adicionar coluna", rolagem horizontal e
564
+ tela vazia. Cada ação aparece quando o callback dela é passado. Durante o
565
+ arraste o quadro mostra uma prévia; ao soltar, `aoMudar` recebe as colunas e
566
+ `aoSoltar` o movimento, que é o que o servidor precisa para gravar.
567
+
568
+ ```tsx
569
+ import { CartaoKanban, Kanban, type ColunaKanban } from '@celke/kole-react';
570
+
571
+ const [colunas, setColunas] = useState<ColunaKanban<Cartao>[]>(inicial);
572
+
573
+ <Kanban
574
+ colunas={colunas}
575
+ aoMudar={setColunas}
576
+ aoSoltar={({ item, de, para }) => api.moverCartao(item.id, para.coluna, para.indice)}
577
+ aoSoltarColuna={({ colunas }) => api.ordenarColunas(colunas.map((c) => c.id))} // liga o arraste das colunas
578
+ filtro={busca ? (c) => c.titulo.includes(busca) : undefined} // esconde sem tirar do lugar
579
+ nomeItem={(c) => c.titulo}
580
+ renderCartao={(c, alca) => (
581
+ <CartaoKanban
582
+ titulo={c.titulo}
583
+ etiquetas={c.etiquetas}
584
+ prioridade={c.prioridade} // 'baixa' | 'media' | 'alta' | 'critica'
585
+ vencimento={c.vencimento} // 'AAAA-MM-DD': atrasado, hoje, em breve
586
+ subtarefas={{ feitas: 2, total: 5 }}
587
+ responsaveis={c.pessoas}
588
+ alca={alca}
589
+ aoAbrir={() => abrirDetalhe(c)}
590
+ />
591
+ )}
592
+ aoAdicionarCartao={(colunaId, titulo) => criarCartao(colunaId, titulo)}
593
+ aoAdicionarColuna={(titulo) => criarColuna(titulo)}
594
+ aoRenomearColuna={renomear}
595
+ aoRecolherColuna={recolher}
596
+ aoExcluirColuna={async (id) => (await confirmar.remover()) && excluir(id)}
597
+ />
598
+ ```
599
+
600
+ `ListaTarefas` mostra tarefas em linhas: marcar como feita (`aoConcluir`),
601
+ situação, prioridade, prazo, etiquetas e responsáveis; `aoReordenar` liga o
602
+ arraste e recebe a lista na ordem nova; `filtro` faz as visões (hoje,
603
+ atrasadas, uma lista) sem perder a ordem das escondidas.
604
+
605
+ Os dois são montados sobre o `QuadroArrastavel`, que também serve sozinho:
606
+ `layout="faixa"`, `podeSoltar` (regra de destino), `visivel`,
607
+ `colunasArrastaveis` e rolagem automática perto das bordas. A demo traz as telas
608
+ completas em Aplicativos › Kanban (listagem de quadros, quadro com filtros e
609
+ detalhe do cartão) e Aplicativos › Tarefas.
610
+
611
+ ### Finanças
612
+
613
+ `Valor` mostra dinheiro com o sentido certo: guarde o valor sempre positivo e
614
+ diga o `tipo` (`receita` em verde com `+`, `despesa` em vermelho com `−`,
615
+ `neutro` para transferência). Dentro de um `ProvedorValores`, o
616
+ `BotaoOcultarValores` esconde todos de uma vez, como nos apps de banco.
617
+
618
+ ```tsx
619
+ import { BotaoOcultarValores, NavegadorPeriodo, ProvedorValores, Valor, periodoDe } from '@celke/kole-react';
620
+
621
+ const [periodo, setPeriodo] = useState(() => periodoDe('mes')); // { tipo, de: 'AAAA-MM-DD', ate }
622
+
623
+ <ProvedorValores chave="financas-ocultos">
624
+ <NavegadorPeriodo valor={periodo} aoMudar={setPeriodo} /> {/* ‹ Novembro de 2026 › */}
625
+ <BotaoOcultarValores />
626
+ <Valor valor={2200} tipo="despesa" forte /> {/* −R$ 2.200,00 */}
627
+ </ProvedorValores>
628
+ ```
629
+
630
+ `GraficoRosca` faz o "despesas por categoria" (com `maxFatias` e `aoEscolher`
631
+ para abrir os lançamentos da fatia) e o `SeletorContexto`, na prop `contexto`
632
+ da `Estrutura`, troca o sistema inteiro entre pessoal e empresa (ou filiais).
633
+ `formatarMoeda`, `dividirEmParcelas` e `somarMoeda` cuidam do sinal de menos,
634
+ dos centavos das parcelas e do erro de ponto flutuante. A demo traz a solução
635
+ completa em Soluções › Finanças.
636
+
637
+ ### Gráficos
638
+
639
+ Desenhados em SVG próprio, sem biblioteca de gráficos. A largura
640
+ acompanha o container (o desenho é refeito no tamanho novo, sem esticar o texto),
641
+ os números e as datas saem em pt-BR (`1.234,5`, `jan/21`) e as cores vêm dos
642
+ tokens `--graf-*`, então funcionam nos temas claro, escuro e sistema.
643
+
644
+ Comum a todos: `titulo` (obrigatório; vira o nome acessível), `descricao` (resumo
645
+ para leitor de tela), `altura` (padrão `280` px), `legenda` (padrão `true`; clicar
646
+ esconde ou mostra a série), `carregando`, `textoVazio` e `verDados` (padrão
647
+ `true`; alterna entre o gráfico e uma `Tabela` com os mesmos valores, o caminho
648
+ completo para quem não enxerga o gráfico). Passe o mouse, toque ou foque o
649
+ gráfico e use as setas do teclado: uma caixa mostra o valor de cada série. `null`
650
+ é "sem dado": falha na linha, barra ausente, célula hachurada, nunca zero.
651
+
652
+ **Animação** (`animacao`, padrão `false`): na entrada, as linhas se desenham da
653
+ esquerda para a direita, as barras crescem a partir do zero e os pontos e as
654
+ células surgem; rótulos e referências chegam depois. É só CSS e roda uma vez,
655
+ quando o gráfico aparece: dados que chegam depois (atualização periódica, por
656
+ exemplo) não reiniciam a animação. Com "reduzir movimento" ligado no sistema
657
+ (`prefers-reduced-motion`), nada se mexe. O `Indicador` também aceita `animacao`
658
+ para o minigráfico da `tendencia`. Prefira deixar desligado em painéis abertos
659
+ todo dia: a animação ajuda em apresentações, mas atrasa a leitura.
660
+
661
+ ```tsx
662
+ import {
663
+ GraficoBarras,
664
+ GraficoCombinado,
665
+ GraficoDispersao,
666
+ GraficoLinha,
667
+ Indicador,
668
+ MapaCalor,
669
+ regressaoLinear,
670
+ } from '@celke/kole-react';
671
+
672
+ const reais = (v: number) => v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL', maximumFractionDigits: 0 });
673
+
674
+ // séries no tempo: faixa sombreada no eixo X e linha de referência
675
+ <GraficoLinha
676
+ titulo="Faturamento mensal"
677
+ dados={[{ mes: '2025-10', valor: 248000 }, { mes: '2025-11', valor: null }, { mes: '2025-12', valor: 342000 }]}
678
+ eixoX={{ chave: 'mes', tipo: 'data' }}
679
+ eixoY={{ rotulo: 'Faturamento', formatar: reais }}
680
+ series={[{ chave: 'valor', rotulo: 'Faturamento' }]}
681
+ area
682
+ faixas={[{ de: '2025-11', ate: '2025-12', rotulo: 'Fim de ano' }]}
683
+ referencias={[{ valor: 260000, rotulo: 'Média mensal' }]}
684
+ />;
685
+
686
+ // barras: várias séries ficam agrupadas; `divergente` pinta acima e abaixo de zero;
687
+ // `orientacao="horizontal"` serve para rankings (rótulo longo quebra em duas linhas)
688
+ <GraficoBarras
689
+ titulo="Faturamento acima ou abaixo da meta"
690
+ dados={[{ mes: '2026-05', diferenca: -12 }, { mes: '2026-06', diferenca: 8.5 }]}
691
+ eixoX={{ chave: 'mes', tipo: 'data' }}
692
+ eixoY={{ unidade: 'mil R$' }}
693
+ series={[{ chave: 'diferenca', rotulo: 'Diferença para a meta' }]}
694
+ divergente
695
+ />;
696
+
697
+ // dois eixos: a legenda diz o eixo de cada série ("Ticket médio (R$, eixo direito)")
698
+ <GraficoCombinado
699
+ titulo="Pedidos e ticket médio"
700
+ dados={[{ mes: '2026-05', pedidos: 1420, ticket: 198.4 }, { mes: '2026-06', pedidos: 1510, ticket: 203.1 }]}
701
+ eixoX={{ chave: 'mes', tipo: 'data' }}
702
+ eixoY={{ rotulo: 'Pedidos' }}
703
+ eixoYDireito={{ rotulo: 'Ticket médio', unidade: 'R$' }}
704
+ barras={[{ chave: 'pedidos', rotulo: 'Pedidos' }]}
705
+ linhas={[{ chave: 'ticket', rotulo: 'Ticket médio' }]}
706
+ />;
707
+
708
+ // um ponto por item; `regressao` desenha a reta e mostra r, r² e n
709
+ <GraficoDispersao
710
+ titulo="Preço × unidades vendidas"
711
+ dados={produtos} // [{ produto: 'Café torrado — 500 g', preco: 18.9, unidades: 610, promocao: false }, …]
712
+ eixoX={{ rotulo: 'Preço', unidade: 'R$' }}
713
+ eixoY={{ rotulo: 'Unidades vendidas' }}
714
+ chaveX="preco"
715
+ chaveY="unidades"
716
+ chaveRotulo="produto"
717
+ chaveDestaque="promocao"
718
+ regressao
719
+ nota="Estatísticas a mais (como o p-valor) são calculadas pelo app."
720
+ />;
721
+
722
+ // grade de células (ex.: hora × dia da semana); com muitas linhas, rola com os rótulos das colunas presos no topo
723
+ <MapaCalor
724
+ titulo="Pedidos por hora e dia da semana"
725
+ linhas={['08h', '09h']}
726
+ colunas={['seg', 'ter']}
727
+ valores={[{ linha: '08h', coluna: 'seg', valor: 72 }, { linha: '08h', coluna: 'ter', valor: null }]}
728
+ unidade="pedidos"
729
+ />;
730
+
731
+ // minigráfico abaixo do valor, na cor do indicador
732
+ <Indicador rotulo="Pedidos em junho" valor={1510} cor="var(--graf-3)" tendencia={[1180, 1240, 1320, 1290, 1420, 1510]} />;
733
+
734
+ // a mesma conta da dispersão, para usar no app: { inclinacao, intercepto, r, r2, n } ou null
735
+ const reta = regressaoLinear([{ x: 4.99, y: 880 }, { x: 18.9, y: 610 }, { x: 39.9, y: 190 }]);
736
+ ```
737
+
738
+ Tipos do eixo X: `categoria` (texto, uma faixa por item), `data` (`Date`,
739
+ timestamp ou texto `aaaa-mm-dd`/`aaaa-mm`; o rótulo se ajusta a dados diários,
740
+ mensais ou anuais) e `numero`. Os rótulos do eixo X nunca se sobrepõem: se não
741
+ couberem, aparece um a cada N. `eixo.formatar` troca o formato dos números
742
+ (eixo, caixa de detalhes e tabela).
743
+
744
+ Tokens (definidos nos dois temas; redefina no CSS do app para mudar a paleta):
745
+
746
+ | Token | Uso |
747
+ |---|---|
748
+ | `--graf-1` a `--graf-6` | Cores das séries, nessa ordem (contraste de 3:1 ou mais contra `--surface`) |
749
+ | `--graf-pos` / `--graf-neg` | Barras divergentes, acima e abaixo de zero |
750
+ | `--graf-seq-1` a `--graf-seq-5` | Escala do mapa de calor, do valor mais baixo ao mais alto |
751
+ | `--graf-grade`, `--graf-eixo`, `--graf-texto` | Linhas de grade, eixos e rótulos |
752
+ | `--graf-faixa` | Faixa sombreada de destaque |
753
+
754
+ **Volume de pontos:** cada gráfico é fluido até cerca de 2.000 pontos. Séries
755
+ maiores devem chegar agregadas (por exemplo, os pedidos de anos somados por mês
756
+ em vez de um ponto por pedido, ou o mapa de calor em vez de uma linha só).
757
+
758
+ Utilitários (lógica pura, também exportados): validadores BR (`isCpf`, `isCnpj`,
759
+ `isCnh`, `isImei`, `validar(tipo, valor)`…), `TIPOS_CAMPO` (máscara +
760
+ placeholder + regra por tipo), `ESTADOS`/`TIPOS_PESSOA`, por extenso
761
+ (`numeroPorExtenso`, `valorMonetarioPorExtenso`, `listaPorExtenso`), texto
762
+ (`normalizar`, `nomeProprio`, `nomeCurto`, `iniciais`, `formatCpfCnpj`), datas
763
+ pt-BR (`parseDataBr`, `formatDataBr`…), `tituloDeEntidade`/`nomeDeArquivo`,
764
+ anexos (`formatBytes`, `tipoDeArquivo`, `iconeArquivo`), senha
765
+ (`validarSenha`, `forcaSenha`), picklist, JWT (`decodificarJwt`, `jwtExpirado`…)
766
+ e `severidadeCor`. A demo do repositório tem uma página interativa de cada um
767
+ (`#/utilitarios`).
768
+
769
+ Convenções:
770
+
771
+ - Campos são controlados por `valor` + `aoMudar`; aceitam `rotulo`, `dica`,
772
+ `erro`, `aviso`, `obrigatorio` e `variante="inline"` (pílula com rótulo dentro,
773
+ para barras de filtro).
774
+ - Severidades: `ok`, `info`, `warn`, `err`, `neutral` (botão: `pri`, `secundario`,
775
+ `ok`, `warn`, `err`).
776
+ - Ícones: [Lucide](https://lucide.dev/icons) (SVG, já incluso no pacote). Passe o
777
+ nome (`icone="save"`, `icone="trash-2"`) ou o componente
778
+ (`import { Rocket } from 'lucide-react'` → `icone={<Rocket />}`). Só os ícones
779
+ registrados entram no bundle; para usar outro pelo nome, registre uma vez:
780
+ `registrarIcones({ rocket: Rocket })`. Um nome não registrado avisa no console
781
+ em desenvolvimento.
782
+ - Pendência de campo: o erro aparece ao **sair** do campo depois de digitar, ou
783
+ quando o `Form` tenta salvar (todos os campos acusam). Um valor carregado de
784
+ fora que não atende ao formato não é recusado: vira aviso âmbar.
785
+ - Grade do `FormSecao`: 4 colunas na página, 3 no modal; `className="lp-col-2"`,
786
+ `lp-col-3` ou `lp-col-full` alarga o campo.
787
+ - Cadastro com vários formulários: passe `menu` (itens com `id`, `rotulo`,
788
+ `icone`, `href`) e `menuAtivo` ao `Form` de cada página; o menu lateral liga
789
+ os formulários entre si, cada um com o próprio Salvar.
790
+
791
+ ## Tema e tokens
792
+
793
+ Toda cor/medida vem de variáveis CSS (os tokens). Para mudar a marca, redefina
794
+ os tokens no CSS do app (a ordem dos imports não importa, veja [CSS](#css)) — por exemplo
795
+ `--navy` (marinho da sidebar), `--pri` (azul de ação) e `--acento-claro` (laranja
796
+ do item ativo). O tema escuro tem valores próprios, então redefina lá também:
797
+
798
+ ```css
799
+ :root {
800
+ --pri: #0f766e;
801
+ --pri-ink: #115e59;
802
+ }
803
+ [data-theme='dark'] {
804
+ --pri: #2dd4bf;
805
+ --pri-ink: #5eead4;
806
+ }
807
+ ```
808
+
809
+ O tema vem do atributo `data-theme` no `<html>`: `light`, `dark` ou `system`
810
+ (segue o modo do sistema operacional, inclusive se ele mudar com a página
811
+ aberta). No `ProvedorUi`, são os temas `claro`, `escuro` e `sistema`
812
+ (`temaInicial`, `definirTema`). Enquanto a pessoa não escolhe, vale o
813
+ `temaInicial`, e sem ele o `sistema`. `escuro` no `usePreferencias()` diz o que
814
+ está na tela. A densidade usa as classes `density-compact`, `density-comfortable` e
815
+ `density-cozy` no `<body>`. O `ProvedorUi` cuida de tudo isso. A lista
816
+ completa de tokens, com os valores dos dois temas, está em
817
+ `@celke/kole-react/tokens.css`.
818
+
819
+ Para usar só os tokens fora do React: `@import '@celke/kole-react/tokens.css';`
820
+ e `<html data-theme="system">` (ou `light`/`dark`) para escolher o tema sem JS.
821
+
822
+ ## CSS
823
+
824
+ Classes CSS usam o prefixo `lp-`, e todo o CSS da biblioteca fica na camada
825
+ `@layer kole`. CSS fora de camada sempre vence CSS em camada, então o CSS do
826
+ app personaliza o kole sem disputar especificidade nem depender da ordem dos
827
+ imports. O lado oposto: uma regra global do app (`a { color: … }`,
828
+ `h1 { font-size: … }`) também atinge os componentes, por isso escope as regras
829
+ de elemento do app (por exemplo, `.conteudo-artigo h1`).
830
+
831
+ ### Com Tailwind 4
832
+
833
+ A ordem das camadas precisa ser declarada **antes** de qualquer CSS, então o
834
+ CSS do app (com o Tailwind) é importado antes do `estilos.css`:
835
+
836
+ ```css
837
+ /* app.css — esta linha vem antes do @import do Tailwind */
838
+ @layer theme, base, kole, components, utilities;
839
+ @import 'tailwindcss';
840
+ @import '@celke/kole-react/tailwind.css'; /* opcional: a ponte abaixo */
841
+ ```
842
+
843
+ ```tsx
844
+ // main.tsx
845
+ import './app.css';
846
+ import '@celke/kole-react/estilos.css';
847
+ ```
848
+
849
+ Assim o kole vence o preflight do Tailwind (botões e campos mantêm o estilo) e
850
+ os utilitários vencem o kole (`<Botao className="mt-4" />` funciona). Se o
851
+ `estilos.css` vier primeiro, a camada `kole` fica abaixo do preflight e os
852
+ componentes perdem o estilo.
853
+
854
+ A ponte `@celke/kole-react/tailwind.css` faz duas coisas:
855
+
856
+ - o `dark:` do Tailwind segue o tema do kole (`data-theme`), e não o do sistema
857
+ operacional, então uma tela com os dois não fica metade clara e metade escura;
858
+ - os tokens viram utilitários com o mesmo nome: `bg-surface`, `text-ink`,
859
+ `text-t3`, `border-border`, `bg-pri`, `text-ok-fg`, `bg-err-bg`,
860
+ `rounded-card`, `rounded-pill`… Como apontam para os tokens, trocam de tema
861
+ junto com o kole. `font-sans` e `font-mono` passam a ser as fontes do kole.
862
+ Para o site público: `max-w-site`, `rounded-site`, `rounded-site-2`,
863
+ `shadow-site`, `text-site-hero`, `text-site-titulo`, `text-site-secao` e, sobre
864
+ o marinho, `bg-navy text-site-escuro-t3`.
865
+
866
+ A densidade não chega aos utilitários: `p-4` e `h-10` são fixos, só os
867
+ componentes `lp-*` mudam com ela.
868
+
869
+ Vale igual para o site e para o sistema: os componentes de site (`EstruturaSite`,
870
+ `DestaqueSite`…) estão no mesmo `estilos.css`, na mesma camada `kole`, então
871
+ `<SecaoSite className="mt-8" />` funciona e o preflight não desfaz o estilo deles.
872
+
873
+ ## Desenvolvimento
874
+
875
+ ```bash
876
+ npm install
877
+ npm run dev # http://localhost:5173 — demo com todos os componentes
878
+ npm run typecheck
879
+ npm run build # dist/index.js + tipos (.d.ts) + dist/estilos.css + dist/tokens.css + dist/tailwind.css
880
+ ```
881
+
882
+ A demo usa o mesmo CSS do pacote (na camada `kole`, com o tema `sistema`), e o
883
+ `demo/demo.css` fica fora da camada, como o CSS de um app.
884
+
885
+ ### Documentação
886
+
887
+ A demo tem a seção **Documentação** (`/#/docs/...`): Instalação, Tema e tokens,
888
+ Tailwind, Carregamento, Changelog e 60 páginas de componente, com exemplos ao vivo, o código
889
+ de cada exemplo e a tabela de props. Fica em `demo/docs/`:
890
+
891
+ - `nav.ts`: o catálogo (categoria → componente). O menu, o índice
892
+ (`/#/docs/componentes`) e as rotas saem dele.
893
+ - `paginas/componentes/<slug>.tsx`: a página de cada componente (`export
894
+ default`), montada com `PaginaComponente`. Componente novo = uma linha no
895
+ catálogo + este arquivo.
896
+ - `exemplos/<slug>/<Nome>.tsx`: cada arquivo é ao mesmo tempo o exemplo que roda
897
+ e o código que a página mostra (os imports de `src` aparecem como
898
+ `@celke/kole-react`). Na página: `<Exemplo id="botao/Cores" titulo="Cores" />`.
899
+ As classes `demo-*` dos exemplos são só de layout da demo.
900
+ - `props.gerado.json`: as props de todas as interfaces exportadas (`BotaoProps`
901
+ vira `Botao`; `OpcoesToast`, `Coluna`… ficam com o próprio nome), lidas dos
902
+ tipos TypeScript: nome, tipo, obrigatória, padrão e o comentário `/** … */` da
903
+ prop como descrição. É gerado por `npm run docs:props`, que já roda no `dev` e
904
+ no `build:demo`; `npm run docs:conferir` falha se estiver desatualizado.
905
+
906
+ ### Teste visual
907
+
908
+ Captura cada página do menu da demo (`demo/nav.ts`) nos temas claro e escuro e
909
+ compara pixel a pixel com uma referência. Também confere que o tema `sistema`,
910
+ com o sistema operacional no escuro, sai igual ao tema escuro. Página nova no
911
+ menu entra no teste sozinha.
912
+
913
+ O print muda de um sistema operacional para outro, então a referência fica em
914
+ `tests/visual/__referencia__/<sistema>/`. Só a de Linux vai para o git: é a que
915
+ o CI usa (`.github/workflows/visual.yml`, na imagem Docker do Playwright), em
916
+ todo PR e na `main`.
917
+
918
+ **Mudança visual de propósito:** o CI do PR vai falhar apontando as páginas que
919
+ mudaram (o relatório com as diferenças fica nos artefatos do workflow). Confira
920
+ e grave a referência nova: Actions → Teste visual → Run workflow, na branch do
921
+ PR, com "atualizar" marcado. O workflow faz o commit da referência na branch.
922
+
923
+ No Windows ou no macOS, o teste usa uma referência local, fora do git:
924
+
925
+ ```bash
926
+ npx playwright install chromium # uma vez
927
+ npm run visual:referencia # ANTES de mexer no CSS: grava a referência
928
+ npm run visual # depois: aponta cada página que mudou
929
+ npx playwright show-report # diferenças lado a lado
930
+ ```
931
+
932
+ Para comparar com uma versão já publicada, sirva a demo dela noutra porta (por
933
+ exemplo, num `git worktree` da tag) e gere a referência com
934
+ `KOLE_URL=http://localhost:5181 npm run visual:referencia`.
935
+
936
+ Para testar uma versão que ainda não está no npm, instale direto do GitHub
937
+ (o `prepare` gera o `dist/` na instalação) ou de um pacote local:
938
+
939
+ ```bash
940
+ npm i github:celkecursos/kole-react
941
+ # ou: gere o .tgz na pasta da biblioteca e instale no app
942
+ npm pack # → celke-kole-react-<versão>.tgz
943
+ npm i ../kole-react/celke-kole-react-<versão>.tgz
944
+ ```
945
+
946
+ Evite `npm i ../kole-react` (pasta): o npm cria um link e o bundler
947
+ pega o React do `node_modules` da biblioteca — dois Reacts no app quebram os hooks
948
+ (_Invalid hook call_). Se precisar do link durante o desenvolvimento, use
949
+ `resolve: { dedupe: ['react', 'react-dom'] }` no `vite.config` do app.
950
+
951
+ Publicar uma versão nova: `npm version patch` (só correção) ou `npm version minor`
952
+ (muda a aparência ou a API — antes da 1.0, o `^0.x` dos apps não pega a minor
953
+ sozinho), registre no [CHANGELOG](CHANGELOG.md) e confira o conteúdo com
954
+ `npm pack --dry-run`. Depois do merge na `main`, publique pelo GitHub: Actions →
955
+ Publicar → Run workflow (`.github/workflows/publish.yml`). O npm confia nesse
956
+ workflow pela publicação confiável (Trusted Publisher, sem token) e o pacote
957
+ sai com o atestado de procedência. O workflow só deixa a versão em espera
958
+ (`npm stage publish`): ela vai ao ar depois que um mantenedor aprova com o
959
+ segundo fator, com `npm stage list @celke/kole-react` e `npm stage approve <id>`
960
+ (ou pelo site do npm). O workflow recusa versão que já está no npm.
961
+
962
+ ## Autor
963
+
964
+ Criado por **Cesar Szpak**.
965
+
966
+ ## Licença
967
+
968
+ MIT © 2026 Celke — veja o arquivo [LICENSE](https://github.com/celkecursos/kole-react/blob/main/LICENSE).