@celke/kole-react 0.3.0 → 0.4.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/README.md CHANGED
@@ -1,401 +1,525 @@
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
-
149
- ### Conversa (chat)
150
-
151
- `Conversa` é a área rolável (acompanha o fim durante o streaming e para de
152
- puxar se a pessoa rolar para cima); `Mensagem` é a bolha; `CampoMensagem` é o
153
- campo de envio (Enter envia, Shift+Enter quebra a linha); `Referencias` lista
154
- as fontes de uma resposta. A conversa ocupa a altura do contêiner pai, e a
155
- busca da resposta (rede, streaming, Markdown) fica com o app.
156
-
157
- ```tsx
158
- import { useState } from 'react';
159
- import {
160
- CampoMensagem,
161
- Conversa,
162
- Mensagem,
163
- Referencias,
164
- type AutorMensagem,
165
- type EstadoMensagem,
166
- type ItemReferencia,
167
- } from '@celke/kole-react';
168
-
169
- interface ItemChat {
170
- id: string;
171
- autor: AutorMensagem;
172
- estado: EstadoMensagem;
173
- texto: string;
174
- hora: Date;
175
- fontes?: ItemReferencia[]; // { id, titulo, detalhe?, trecho?, href? }
176
- }
177
-
178
- interface ChatProps {
179
- mensagens: ItemChat[];
180
- gerando: boolean;
181
- enviar: (texto: string) => void;
182
- parar: () => void;
183
- }
184
-
185
- export function Chat({ mensagens, gerando, enviar, parar }: ChatProps) {
186
- const [texto, setTexto] = useState('');
187
- return (
188
- <div style={{ height: '32rem' }}>
189
- <Conversa
190
- campo={
191
- <CampoMensagem
192
- valor={texto}
193
- aoMudar={setTexto}
194
- aoEnviar={(t) => {
195
- enviar(t);
196
- setTexto('');
197
- }}
198
- gerando={gerando}
199
- aoParar={parar}
200
- />
201
- }
202
- >
203
- {mensagens.map((m) => (
204
- <Mensagem
205
- key={m.id}
206
- autor={m.autor} // 'usuario' | 'assistente' | 'sistema'
207
- estado={m.estado} // 'enviando' | 'gerando' | 'concluida' | 'erro'
208
- hora={m.hora}
209
- copiavel={m.autor === 'assistente'}
210
- rodape={m.fontes && <Referencias itens={m.fontes} />}
211
- >
212
- {m.texto}
213
- </Mensagem>
214
- ))}
215
- </Conversa>
216
- </div>
217
- );
218
- }
219
- ```
220
-
221
- Utilitários (lógica pura, também exportados): validadores BR (`isCpf`, `isCnpj`,
222
- `isCnh`, `isImei`, `validar(tipo, valor)`…), `TIPOS_CAMPO` (máscara +
223
- placeholder + regra por tipo), `ESTADOS`/`TIPOS_PESSOA`, por extenso
224
- (`numeroPorExtenso`, `valorMonetarioPorExtenso`, `listaPorExtenso`), texto
225
- (`normalizar`, `nomeProprio`, `nomeCurto`, `iniciais`, `formatCpfCnpj`), datas
226
- pt-BR (`parseDataBr`, `formatDataBr`…), `tituloDeEntidade`/`nomeDeArquivo`,
227
- anexos (`formatBytes`, `tipoDeArquivo`, `iconeArquivo`), senha
228
- (`validarSenha`, `forcaSenha`), picklist, JWT (`decodificarJwt`, `jwtExpirado`…)
229
- e `severidadeCor`. A demo do repositório tem uma página interativa de cada um
230
- (`#/utilitarios`).
231
-
232
- Convenções:
233
-
234
- - Campos são controlados por `valor` + `aoMudar`; aceitam `rotulo`, `dica`,
235
- `erro`, `aviso`, `obrigatorio` e `variante="inline"` (pílula com rótulo dentro,
236
- para barras de filtro).
237
- - Severidades: `ok`, `info`, `warn`, `err`, `neutral` (botão: `pri`, `secundario`,
238
- `ok`, `warn`, `err`).
239
- - Ícones: [Lucide](https://lucide.dev/icons) (SVG, já incluso no pacote). Passe o
240
- nome (`icone="save"`, `icone="trash-2"`) ou o componente
241
- (`import { Rocket } from 'lucide-react'` → `icone={<Rocket />}`). Só os ícones
242
- registrados entram no bundle; para usar outro pelo nome, registre uma vez:
243
- `registrarIcones({ rocket: Rocket })`. Um nome não registrado avisa no console
244
- em desenvolvimento.
245
- - Pendência de campo: o erro aparece ao **sair** do campo depois de digitar, ou
246
- quando o `Form` tenta salvar (todos os campos acusam). Um valor carregado de
247
- fora que não atende ao formato não é recusado: vira aviso âmbar.
248
- - Grade do `FormSecao`: 4 colunas na página, 3 no modal; `className="lp-col-2"`,
249
- `lp-col-3` ou `lp-col-full` alarga o campo.
250
- - Cadastro com vários formulários: passe `menu` (itens com `id`, `rotulo`,
251
- `icone`, `href`) e `menuAtivo` ao `Form` de cada página; o menu lateral liga
252
- os formulários entre si, cada um com o próprio Salvar.
253
-
254
- ## Tema e tokens
255
-
256
- Toda cor/medida vem de variáveis CSS (os tokens). Para mudar a marca, redefina
257
- os tokens no CSS do app (a ordem dos imports não importa, veja [CSS](#css)) — por exemplo
258
- `--navy` (marinho da sidebar), `--pri` (azul de ação) e `--acento-claro` (laranja
259
- do item ativo). O tema escuro tem valores próprios, então redefina lá também:
260
-
261
- ```css
262
- :root {
263
- --pri: #0f766e;
264
- --pri-ink: #115e59;
265
- }
266
- [data-theme='dark'] {
267
- --pri: #2dd4bf;
268
- --pri-ink: #5eead4;
269
- }
270
- ```
271
-
272
- O tema vem do atributo `data-theme` no `<html>`: `light`, `dark` ou `system`
273
- (segue o modo do sistema operacional, inclusive se ele mudar com a página
274
- aberta). No `ProvedorUi`, são os temas `claro`, `escuro` e `sistema`
275
- (`temaInicial`, `definirTema`). Enquanto a pessoa não escolhe, vale o
276
- `temaInicial`, e sem ele o `sistema`. `escuro` no `usePreferencias()` diz o que
277
- está na tela. A densidade usa as classes `density-compact`, `density-comfortable` e
278
- `density-cozy` no `<body>`. O `ProvedorUi` cuida de tudo isso. A lista
279
- completa de tokens, com os valores dos dois temas, está em
280
- `@celke/kole-react/tokens.css`.
281
-
282
- Para usar só os tokens fora do React: `@import '@celke/kole-react/tokens.css';`
283
- e `<html data-theme="system">` (ou `light`/`dark`) para escolher o tema sem JS.
284
-
285
- ## CSS
286
-
287
- Classes CSS usam o prefixo `lp-`, e todo o CSS da biblioteca fica na camada
288
- `@layer kole`. CSS fora de camada sempre vence CSS em camada, então o CSS do
289
- app personaliza o kole sem disputar especificidade nem depender da ordem dos
290
- imports. O lado oposto: uma regra global do app (`a { color: … }`,
291
- `h1 { font-size: … }`) também atinge os componentes, por isso escope as regras
292
- de elemento do app (por exemplo, `.conteudo-artigo h1`).
293
-
294
- ### Com Tailwind 4
295
-
296
- A ordem das camadas precisa ser declarada **antes** de qualquer CSS, então o
297
- CSS do app (com o Tailwind) é importado antes do `estilos.css`:
298
-
299
- ```css
300
- /* app.css — esta linha vem antes do @import do Tailwind */
301
- @layer theme, base, kole, components, utilities;
302
- @import 'tailwindcss';
303
- @import '@celke/kole-react/tailwind.css'; /* opcional: a ponte abaixo */
304
- ```
305
-
306
- ```tsx
307
- // main.tsx
308
- import './app.css';
309
- import '@celke/kole-react/estilos.css';
310
- ```
311
-
312
- Assim o kole vence o preflight do Tailwind (botões e campos mantêm o estilo) e
313
- os utilitários vencem o kole (`<Botao className="mt-4" />` funciona). Se o
314
- `estilos.css` vier primeiro, a camada `kole` fica abaixo do preflight e os
315
- componentes perdem o estilo.
316
-
317
- A ponte `@celke/kole-react/tailwind.css` faz duas coisas:
318
-
319
- - o `dark:` do Tailwind segue o tema do kole (`data-theme`), e não o do sistema
320
- operacional, então uma tela com os dois não fica metade clara e metade escura;
321
- - os tokens viram utilitários com o mesmo nome: `bg-surface`, `text-ink`,
322
- `text-t3`, `border-border`, `bg-pri`, `text-ok-fg`, `bg-err-bg`,
323
- `rounded-card`, `rounded-pill`… Como apontam para os tokens, trocam de tema
324
- junto com o kole. `font-sans` e `font-mono` passam a ser as fontes do kole.
325
-
326
- A densidade não chega aos utilitários: `p-4` e `h-10` são fixos, só os
327
- componentes `lp-*` mudam com ela.
328
-
329
- ## Desenvolvimento
330
-
331
- ```bash
332
- npm install
333
- npm run dev # http://localhost:5173 — demo com todos os componentes
334
- npm run typecheck
335
- npm run build # dist/index.js + tipos (.d.ts) + dist/estilos.css + dist/tokens.css + dist/tailwind.css
336
- ```
337
-
338
- A demo usa o mesmo CSS do pacote (na camada `kole`, com o tema `sistema`), e o
339
- `demo/demo.css` fica fora da camada, como o CSS de um app.
340
-
341
- ### Teste visual
342
-
343
- Captura cada página do menu da demo (`demo/nav.ts`) nos temas claro e escuro e
344
- compara pixel a pixel com uma referência. Também confere que o tema `sistema`,
345
- com o sistema operacional no escuro, sai igual ao tema escuro. Página nova no
346
- menu entra no teste sozinha.
347
-
348
- O print muda de um sistema operacional para outro, então a referência fica em
349
- `tests/visual/__referencia__/<sistema>/`. Só a de Linux vai para o git: é a que
350
- o CI usa (`.github/workflows/visual.yml`, na imagem Docker do Playwright), em
351
- todo PR e na `main`.
352
-
353
- **Mudança visual de propósito:** o CI do PR vai falhar apontando as páginas que
354
- mudaram (o relatório com as diferenças fica nos artefatos do workflow). Confira
355
- e grave a referência nova: Actions → Teste visual → Run workflow, na branch do
356
- PR, com "atualizar" marcado. O workflow faz o commit da referência na branch.
357
-
358
- No Windows ou no macOS, o teste usa uma referência local, fora do git:
359
-
360
- ```bash
361
- npx playwright install chromium # uma vez
362
- npm run visual:referencia # ANTES de mexer no CSS: grava a referência
363
- npm run visual # depois: aponta cada página que mudou
364
- npx playwright show-report # diferenças lado a lado
365
- ```
366
-
367
- Para comparar com uma versão já publicada, sirva a demo dela noutra porta (por
368
- exemplo, num `git worktree` da tag) e gere a referência com
369
- `KOLE_URL=http://localhost:5181 npm run visual:referencia`.
370
-
371
- Para testar uma versão que ainda não está no npm, instale direto do GitHub
372
- (o `prepare` gera o `dist/` na instalação) ou de um pacote local:
373
-
374
- ```bash
375
- npm i github:celkecursos/kole-react
376
- # ou: gere o .tgz na pasta da biblioteca e instale no app
377
- npm pack # → celke-kole-react-<versão>.tgz
378
- npm i ../kole-react/celke-kole-react-<versão>.tgz
379
- ```
380
-
381
- Evite `npm i ../kole-react` (pasta): o npm cria um link e o bundler
382
- pega o React do `node_modules` da biblioteca — dois Reacts no app quebram os hooks
383
- (_Invalid hook call_). Se precisar do link durante o desenvolvimento, use
384
- `resolve: { dedupe: ['react', 'react-dom'] }` no `vite.config` do app.
385
-
386
- Publicar uma versão nova: `npm version patch` (só correção) ou `npm version minor`
387
- (muda a aparência ou a API — antes da 1.0, o `^0.x` dos apps não pega a minor
388
- sozinho), registre no [CHANGELOG](CHANGELOG.md) e confira o conteúdo com
389
- `npm pack --dry-run`. Depois do merge na `main`, publique pelo GitHub: Actions →
390
- Publicar → Run workflow (`.github/workflows/publish.yml`). O npm confia nesse
391
- workflow pela publicação confiável (Trusted Publisher, sem token nem segundo
392
- fator) e o pacote sai com o atestado de procedência. O workflow recusa versão
393
- que já está no npm.
394
-
395
- ## Autor
396
-
397
- Criado por **Cesar Szpak**.
398
-
399
- ## Licença
400
-
401
- 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
+ ### 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
+ | Gráficos | `GraficoLinha`, `GraficoBarras`, `GraficoCombinado`, `GraficoDispersao`, `MapaCalor`, `Minigrafico` (e `tendencia` no `Indicador`), `regressaoLinear` |
149
+
150
+ ### Conversa (chat)
151
+
152
+ `Conversa` é a área rolável (acompanha o fim durante o streaming e para de
153
+ puxar se a pessoa rolar para cima); `Mensagem` é a bolha; `CampoMensagem` é o
154
+ campo de envio (Enter envia, Shift+Enter quebra a linha); `Referencias` lista
155
+ as fontes de uma resposta. A conversa ocupa a altura do contêiner pai, e a
156
+ busca da resposta (rede, streaming, Markdown) fica com o app.
157
+
158
+ ```tsx
159
+ import { useState } from 'react';
160
+ import {
161
+ CampoMensagem,
162
+ Conversa,
163
+ Mensagem,
164
+ Referencias,
165
+ type AutorMensagem,
166
+ type EstadoMensagem,
167
+ type ItemReferencia,
168
+ } from '@celke/kole-react';
169
+
170
+ interface ItemChat {
171
+ id: string;
172
+ autor: AutorMensagem;
173
+ estado: EstadoMensagem;
174
+ texto: string;
175
+ hora: Date;
176
+ fontes?: ItemReferencia[]; // { id, titulo, detalhe?, trecho?, href? }
177
+ }
178
+
179
+ interface ChatProps {
180
+ mensagens: ItemChat[];
181
+ gerando: boolean;
182
+ enviar: (texto: string) => void;
183
+ parar: () => void;
184
+ }
185
+
186
+ export function Chat({ mensagens, gerando, enviar, parar }: ChatProps) {
187
+ const [texto, setTexto] = useState('');
188
+ return (
189
+ <div style={{ height: '32rem' }}>
190
+ <Conversa
191
+ campo={
192
+ <CampoMensagem
193
+ valor={texto}
194
+ aoMudar={setTexto}
195
+ aoEnviar={(t) => {
196
+ enviar(t);
197
+ setTexto('');
198
+ }}
199
+ gerando={gerando}
200
+ aoParar={parar}
201
+ />
202
+ }
203
+ >
204
+ {mensagens.map((m) => (
205
+ <Mensagem
206
+ key={m.id}
207
+ autor={m.autor} // 'usuario' | 'assistente' | 'sistema'
208
+ estado={m.estado} // 'enviando' | 'gerando' | 'concluida' | 'erro'
209
+ hora={m.hora}
210
+ copiavel={m.autor === 'assistente'}
211
+ rodape={m.fontes && <Referencias itens={m.fontes} />}
212
+ >
213
+ {m.texto}
214
+ </Mensagem>
215
+ ))}
216
+ </Conversa>
217
+ </div>
218
+ );
219
+ }
220
+ ```
221
+
222
+ ### Gráficos
223
+
224
+ Desenhados em SVG próprio, sem biblioteca de gráficos. A largura
225
+ acompanha o container (o desenho é refeito no tamanho novo, sem esticar o texto),
226
+ os números e as datas saem em pt-BR (`1.234,5`, `jan/21`) e as cores vêm dos
227
+ tokens `--graf-*`, então funcionam nos temas claro, escuro e sistema.
228
+
229
+ Comum a todos: `titulo` (obrigatório; vira o nome acessível), `descricao` (resumo
230
+ para leitor de tela), `altura` (padrão `280` px), `legenda` (padrão `true`; clicar
231
+ esconde ou mostra a série), `carregando`, `textoVazio` e `verDados` (padrão
232
+ `true`; alterna entre o gráfico e uma `Tabela` com os mesmos valores, o caminho
233
+ completo para quem não enxerga o gráfico). Passe o mouse, toque ou foque o
234
+ gráfico e use as setas do teclado: uma caixa mostra o valor de cada série. `null`
235
+ é "sem dado": falha na linha, barra ausente, célula hachurada, nunca zero.
236
+
237
+ **Animação** (`animacao`, padrão `false`): na entrada, as linhas se desenham da
238
+ esquerda para a direita, as barras crescem a partir do zero e os pontos e as
239
+ células surgem; rótulos e referências chegam depois. É só CSS e roda uma vez,
240
+ quando o gráfico aparece: dados que chegam depois (atualização periódica, por
241
+ exemplo) não reiniciam a animação. Com "reduzir movimento" ligado no sistema
242
+ (`prefers-reduced-motion`), nada se mexe. O `Indicador` também aceita `animacao`
243
+ para o minigráfico da `tendencia`. Prefira deixar desligado em painéis abertos
244
+ todo dia: a animação ajuda em apresentações, mas atrasa a leitura.
245
+
246
+ ```tsx
247
+ import {
248
+ GraficoBarras,
249
+ GraficoCombinado,
250
+ GraficoDispersao,
251
+ GraficoLinha,
252
+ Indicador,
253
+ MapaCalor,
254
+ regressaoLinear,
255
+ } from '@celke/kole-react';
256
+
257
+ const reais = (v: number) => v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL', maximumFractionDigits: 0 });
258
+
259
+ // séries no tempo: faixa sombreada no eixo X e linha de referência
260
+ <GraficoLinha
261
+ titulo="Faturamento mensal"
262
+ dados={[{ mes: '2025-10', valor: 248000 }, { mes: '2025-11', valor: null }, { mes: '2025-12', valor: 342000 }]}
263
+ eixoX={{ chave: 'mes', tipo: 'data' }}
264
+ eixoY={{ rotulo: 'Faturamento', formatar: reais }}
265
+ series={[{ chave: 'valor', rotulo: 'Faturamento' }]}
266
+ area
267
+ faixas={[{ de: '2025-11', ate: '2025-12', rotulo: 'Fim de ano' }]}
268
+ referencias={[{ valor: 260000, rotulo: 'Média mensal' }]}
269
+ />;
270
+
271
+ // barras: várias séries ficam agrupadas; `divergente` pinta acima e abaixo de zero;
272
+ // `orientacao="horizontal"` serve para rankings (rótulo longo quebra em duas linhas)
273
+ <GraficoBarras
274
+ titulo="Faturamento acima ou abaixo da meta"
275
+ dados={[{ mes: '2026-05', diferenca: -12 }, { mes: '2026-06', diferenca: 8.5 }]}
276
+ eixoX={{ chave: 'mes', tipo: 'data' }}
277
+ eixoY={{ unidade: 'mil R$' }}
278
+ series={[{ chave: 'diferenca', rotulo: 'Diferença para a meta' }]}
279
+ divergente
280
+ />;
281
+
282
+ // dois eixos: a legenda diz o eixo de cada série ("Ticket médio (R$, eixo direito)")
283
+ <GraficoCombinado
284
+ titulo="Pedidos e ticket médio"
285
+ dados={[{ mes: '2026-05', pedidos: 1420, ticket: 198.4 }, { mes: '2026-06', pedidos: 1510, ticket: 203.1 }]}
286
+ eixoX={{ chave: 'mes', tipo: 'data' }}
287
+ eixoY={{ rotulo: 'Pedidos' }}
288
+ eixoYDireito={{ rotulo: 'Ticket médio', unidade: 'R$' }}
289
+ barras={[{ chave: 'pedidos', rotulo: 'Pedidos' }]}
290
+ linhas={[{ chave: 'ticket', rotulo: 'Ticket médio' }]}
291
+ />;
292
+
293
+ // um ponto por item; `regressao` desenha a reta e mostra r, r² e n
294
+ <GraficoDispersao
295
+ titulo="Preço × unidades vendidas"
296
+ dados={produtos} // [{ produto: 'Café torrado — 500 g', preco: 18.9, unidades: 610, promocao: false }, …]
297
+ eixoX={{ rotulo: 'Preço', unidade: 'R$' }}
298
+ eixoY={{ rotulo: 'Unidades vendidas' }}
299
+ chaveX="preco"
300
+ chaveY="unidades"
301
+ chaveRotulo="produto"
302
+ chaveDestaque="promocao"
303
+ regressao
304
+ nota="Estatísticas a mais (como o p-valor) são calculadas pelo app."
305
+ />;
306
+
307
+ // grade de células (ex.: hora × dia da semana); com muitas linhas, rola com os rótulos das colunas presos no topo
308
+ <MapaCalor
309
+ titulo="Pedidos por hora e dia da semana"
310
+ linhas={['08h', '09h']}
311
+ colunas={['seg', 'ter']}
312
+ valores={[{ linha: '08h', coluna: 'seg', valor: 72 }, { linha: '08h', coluna: 'ter', valor: null }]}
313
+ unidade="pedidos"
314
+ />;
315
+
316
+ // minigráfico abaixo do valor, na cor do indicador
317
+ <Indicador rotulo="Pedidos em junho" valor={1510} cor="var(--graf-3)" tendencia={[1180, 1240, 1320, 1290, 1420, 1510]} />;
318
+
319
+ // a mesma conta da dispersão, para usar no app: { inclinacao, intercepto, r, r2, n } ou null
320
+ const reta = regressaoLinear([{ x: 4.99, y: 880 }, { x: 18.9, y: 610 }, { x: 39.9, y: 190 }]);
321
+ ```
322
+
323
+ Tipos do eixo X: `categoria` (texto, uma faixa por item), `data` (`Date`,
324
+ timestamp ou texto `aaaa-mm-dd`/`aaaa-mm`; o rótulo se ajusta a dados diários,
325
+ mensais ou anuais) e `numero`. Os rótulos do eixo X nunca se sobrepõem: se não
326
+ couberem, aparece um a cada N. `eixo.formatar` troca o formato dos números
327
+ (eixo, caixa de detalhes e tabela).
328
+
329
+ Tokens (definidos nos dois temas; redefina no CSS do app para mudar a paleta):
330
+
331
+ | Token | Uso |
332
+ |---|---|
333
+ | `--graf-1` a `--graf-6` | Cores das séries, nessa ordem (contraste de 3:1 ou mais contra `--surface`) |
334
+ | `--graf-pos` / `--graf-neg` | Barras divergentes, acima e abaixo de zero |
335
+ | `--graf-seq-1` a `--graf-seq-5` | Escala do mapa de calor, do valor mais baixo ao mais alto |
336
+ | `--graf-grade`, `--graf-eixo`, `--graf-texto` | Linhas de grade, eixos e rótulos |
337
+ | `--graf-faixa` | Faixa sombreada de destaque |
338
+
339
+ **Volume de pontos:** cada gráfico é fluido até cerca de 2.000 pontos. Séries
340
+ maiores devem chegar agregadas (por exemplo, os pedidos de anos somados por mês
341
+ em vez de um ponto por pedido, ou o mapa de calor em vez de uma linha só).
342
+
343
+ Utilitários (lógica pura, também exportados): validadores BR (`isCpf`, `isCnpj`,
344
+ `isCnh`, `isImei`, `validar(tipo, valor)`…), `TIPOS_CAMPO` (máscara +
345
+ placeholder + regra por tipo), `ESTADOS`/`TIPOS_PESSOA`, por extenso
346
+ (`numeroPorExtenso`, `valorMonetarioPorExtenso`, `listaPorExtenso`), texto
347
+ (`normalizar`, `nomeProprio`, `nomeCurto`, `iniciais`, `formatCpfCnpj`), datas
348
+ pt-BR (`parseDataBr`, `formatDataBr`…), `tituloDeEntidade`/`nomeDeArquivo`,
349
+ anexos (`formatBytes`, `tipoDeArquivo`, `iconeArquivo`), senha
350
+ (`validarSenha`, `forcaSenha`), picklist, JWT (`decodificarJwt`, `jwtExpirado`…)
351
+ e `severidadeCor`. A demo do repositório tem uma página interativa de cada um
352
+ (`#/utilitarios`).
353
+
354
+ Convenções:
355
+
356
+ - Campos são controlados por `valor` + `aoMudar`; aceitam `rotulo`, `dica`,
357
+ `erro`, `aviso`, `obrigatorio` e `variante="inline"` (pílula com rótulo dentro,
358
+ para barras de filtro).
359
+ - Severidades: `ok`, `info`, `warn`, `err`, `neutral` (botão: `pri`, `secundario`,
360
+ `ok`, `warn`, `err`).
361
+ - Ícones: [Lucide](https://lucide.dev/icons) (SVG, já incluso no pacote). Passe o
362
+ nome (`icone="save"`, `icone="trash-2"`) ou o componente
363
+ (`import { Rocket } from 'lucide-react'` → `icone={<Rocket />}`). Só os ícones
364
+ registrados entram no bundle; para usar outro pelo nome, registre uma vez:
365
+ `registrarIcones({ rocket: Rocket })`. Um nome não registrado avisa no console
366
+ em desenvolvimento.
367
+ - Pendência de campo: o erro aparece ao **sair** do campo depois de digitar, ou
368
+ quando o `Form` tenta salvar (todos os campos acusam). Um valor carregado de
369
+ fora que não atende ao formato não é recusado: vira aviso âmbar.
370
+ - Grade do `FormSecao`: 4 colunas na página, 3 no modal; `className="lp-col-2"`,
371
+ `lp-col-3` ou `lp-col-full` alarga o campo.
372
+ - Cadastro com vários formulários: passe `menu` (itens com `id`, `rotulo`,
373
+ `icone`, `href`) e `menuAtivo` ao `Form` de cada página; o menu lateral liga
374
+ os formulários entre si, cada um com o próprio Salvar.
375
+
376
+ ## Tema e tokens
377
+
378
+ Toda cor/medida vem de variáveis CSS (os tokens). Para mudar a marca, redefina
379
+ os tokens no CSS do app (a ordem dos imports não importa, veja [CSS](#css)) — por exemplo
380
+ `--navy` (marinho da sidebar), `--pri` (azul de ação) e `--acento-claro` (laranja
381
+ do item ativo). O tema escuro tem valores próprios, então redefina lá também:
382
+
383
+ ```css
384
+ :root {
385
+ --pri: #0f766e;
386
+ --pri-ink: #115e59;
387
+ }
388
+ [data-theme='dark'] {
389
+ --pri: #2dd4bf;
390
+ --pri-ink: #5eead4;
391
+ }
392
+ ```
393
+
394
+ O tema vem do atributo `data-theme` no `<html>`: `light`, `dark` ou `system`
395
+ (segue o modo do sistema operacional, inclusive se ele mudar com a página
396
+ aberta). No `ProvedorUi`, são os temas `claro`, `escuro` e `sistema`
397
+ (`temaInicial`, `definirTema`). Enquanto a pessoa não escolhe, vale o
398
+ `temaInicial`, e sem ele o `sistema`. `escuro` no `usePreferencias()` diz o que
399
+ está na tela. A densidade usa as classes `density-compact`, `density-comfortable` e
400
+ `density-cozy` no `<body>`. O `ProvedorUi` cuida de tudo isso. A lista
401
+ completa de tokens, com os valores dos dois temas, está em
402
+ `@celke/kole-react/tokens.css`.
403
+
404
+ Para usar só os tokens fora do React: `@import '@celke/kole-react/tokens.css';`
405
+ e `<html data-theme="system">` (ou `light`/`dark`) para escolher o tema sem JS.
406
+
407
+ ## CSS
408
+
409
+ Classes CSS usam o prefixo `lp-`, e todo o CSS da biblioteca fica na camada
410
+ `@layer kole`. CSS fora de camada sempre vence CSS em camada, então o CSS do
411
+ app personaliza o kole sem disputar especificidade nem depender da ordem dos
412
+ imports. O lado oposto: uma regra global do app (`a { color: … }`,
413
+ `h1 { font-size: … }`) também atinge os componentes, por isso escope as regras
414
+ de elemento do app (por exemplo, `.conteudo-artigo h1`).
415
+
416
+ ### Com Tailwind 4
417
+
418
+ A ordem das camadas precisa ser declarada **antes** de qualquer CSS, então o
419
+ CSS do app (com o Tailwind) é importado antes do `estilos.css`:
420
+
421
+ ```css
422
+ /* app.css — esta linha vem antes do @import do Tailwind */
423
+ @layer theme, base, kole, components, utilities;
424
+ @import 'tailwindcss';
425
+ @import '@celke/kole-react/tailwind.css'; /* opcional: a ponte abaixo */
426
+ ```
427
+
428
+ ```tsx
429
+ // main.tsx
430
+ import './app.css';
431
+ import '@celke/kole-react/estilos.css';
432
+ ```
433
+
434
+ Assim o kole vence o preflight do Tailwind (botões e campos mantêm o estilo) e
435
+ os utilitários vencem o kole (`<Botao className="mt-4" />` funciona). Se o
436
+ `estilos.css` vier primeiro, a camada `kole` fica abaixo do preflight e os
437
+ componentes perdem o estilo.
438
+
439
+ A ponte `@celke/kole-react/tailwind.css` faz duas coisas:
440
+
441
+ - o `dark:` do Tailwind segue o tema do kole (`data-theme`), e não o do sistema
442
+ operacional, então uma tela com os dois não fica metade clara e metade escura;
443
+ - os tokens viram utilitários com o mesmo nome: `bg-surface`, `text-ink`,
444
+ `text-t3`, `border-border`, `bg-pri`, `text-ok-fg`, `bg-err-bg`,
445
+ `rounded-card`, `rounded-pill`… Como apontam para os tokens, trocam de tema
446
+ junto com o kole. `font-sans` e `font-mono` passam a ser as fontes do kole.
447
+
448
+ A densidade não chega aos utilitários: `p-4` e `h-10` são fixos, só os
449
+ componentes `lp-*` mudam com ela.
450
+
451
+ ## Desenvolvimento
452
+
453
+ ```bash
454
+ npm install
455
+ npm run dev # http://localhost:5173 — demo com todos os componentes
456
+ npm run typecheck
457
+ npm run build # dist/index.js + tipos (.d.ts) + dist/estilos.css + dist/tokens.css + dist/tailwind.css
458
+ ```
459
+
460
+ A demo usa o mesmo CSS do pacote (na camada `kole`, com o tema `sistema`), e o
461
+ `demo/demo.css` fica fora da camada, como o CSS de um app.
462
+
463
+ ### Teste visual
464
+
465
+ Captura cada página do menu da demo (`demo/nav.ts`) nos temas claro e escuro e
466
+ compara pixel a pixel com uma referência. Também confere que o tema `sistema`,
467
+ com o sistema operacional no escuro, sai igual ao tema escuro. Página nova no
468
+ menu entra no teste sozinha.
469
+
470
+ O print muda de um sistema operacional para outro, então a referência fica em
471
+ `tests/visual/__referencia__/<sistema>/`. Só a de Linux vai para o git: é a que
472
+ o CI usa (`.github/workflows/visual.yml`, na imagem Docker do Playwright), em
473
+ todo PR e na `main`.
474
+
475
+ **Mudança visual de propósito:** o CI do PR vai falhar apontando as páginas que
476
+ mudaram (o relatório com as diferenças fica nos artefatos do workflow). Confira
477
+ e grave a referência nova: Actions → Teste visual → Run workflow, na branch do
478
+ PR, com "atualizar" marcado. O workflow faz o commit da referência na branch.
479
+
480
+ No Windows ou no macOS, o teste usa uma referência local, fora do git:
481
+
482
+ ```bash
483
+ npx playwright install chromium # uma vez
484
+ npm run visual:referencia # ANTES de mexer no CSS: grava a referência
485
+ npm run visual # depois: aponta cada página que mudou
486
+ npx playwright show-report # diferenças lado a lado
487
+ ```
488
+
489
+ Para comparar com uma versão já publicada, sirva a demo dela noutra porta (por
490
+ exemplo, num `git worktree` da tag) e gere a referência com
491
+ `KOLE_URL=http://localhost:5181 npm run visual:referencia`.
492
+
493
+ Para testar uma versão que ainda não está no npm, instale direto do GitHub
494
+ (o `prepare` gera o `dist/` na instalação) ou de um pacote local:
495
+
496
+ ```bash
497
+ npm i github:celkecursos/kole-react
498
+ # ou: gere o .tgz na pasta da biblioteca e instale no app
499
+ npm pack # → celke-kole-react-<versão>.tgz
500
+ npm i ../kole-react/celke-kole-react-<versão>.tgz
501
+ ```
502
+
503
+ Evite `npm i ../kole-react` (pasta): o npm cria um link e o bundler
504
+ pega o React do `node_modules` da biblioteca — dois Reacts no app quebram os hooks
505
+ (_Invalid hook call_). Se precisar do link durante o desenvolvimento, use
506
+ `resolve: { dedupe: ['react', 'react-dom'] }` no `vite.config` do app.
507
+
508
+ Publicar uma versão nova: `npm version patch` (só correção) ou `npm version minor`
509
+ (muda a aparência ou a API — antes da 1.0, o `^0.x` dos apps não pega a minor
510
+ sozinho), registre no [CHANGELOG](CHANGELOG.md) e confira o conteúdo com
511
+ `npm pack --dry-run`. Depois do merge na `main`, publique pelo GitHub: Actions →
512
+ Publicar → Run workflow (`.github/workflows/publish.yml`). O npm confia nesse
513
+ workflow pela publicação confiável (Trusted Publisher, sem token) e o pacote
514
+ sai com o atestado de procedência. O workflow só deixa a versão em espera
515
+ (`npm stage publish`): ela vai ao ar depois que um mantenedor aprova com o
516
+ segundo fator, com `npm stage list @celke/kole-react` e `npm stage approve <id>`
517
+ (ou pelo site do npm). O workflow recusa versão que já está no npm.
518
+
519
+ ## Autor
520
+
521
+ Criado por **Cesar Szpak**.
522
+
523
+ ## Licença
524
+
525
+ MIT © 2026 Celke — veja o arquivo [LICENSE](https://github.com/celkecursos/kole-react/blob/main/LICENSE).