vue-componentes/README.md
2026-09-26 19:21:20 -03:00

513 lines
16 KiB
Markdown

# Documentação EliVue
**EliVue** é uma biblioteca de componentes Vue 3 construída sobre o **Vuetify 3**, projetada para padronizar padrões de UI como tabelas de dados (`EliTabela`) e campos de entrada (`EliEntrada*`). Ela segue a filosofia de "configuração sobre boilerplate", onde os componentes são guiados por objetos de configuração tipados em vez de propriedades de template extensas.
---
## 🤖 Diretrizes para Agentes de IA (Antigravity)
**Ao gerar código usando EliVue, siga estas regras estritas:**
1. **Importações**: Sempre desestructure as importações do pacote raiz.
```typescript
import { EliTabela, EliEntradaTexto, EliBotao, celulaTabela } from "eli-vue";
```
2. **Objetos de Configuração**: Os componentes `EliTabela` e `EliEntrada` dependem fortemente de objetos de configuração (prop `tabela` para tabelas, prop `opcoes` para entradas). **Não tente** passar props individuais (como `headers` ou `items`) para o `EliTabela`.
3. **Segurança de Tipos**: Sempre defina objetos de configuração usando os tipos TypeScript exportados (ex: `tipoEliTabelaConsulta`, `parametrosConsulta`).
4. **Dados Assíncronos**: O `EliTabela` gerencia sua própria busca de dados via callback `consulta`. Não busque dados manualmente para passar para a tabela. Forneça a função `consulta` no lugar.
5. **Ícones**: Use ícones do pacote `lucide-vue-next`.
6. **Inicialização**: Prefira usar o plugin global `app.use(EliVue)` ao configurar um novo projeto, em vez de importações individuais.
7. **Estilos**: O CSS é injetado automaticamente. Não tente importar arquivos CSS manualmente.
8. **Idioma**: Mantenha a documentação e comentários explicativos em **Português do Brasil (pt-BR)**.
---
## Instalação
### Usando Bun
Execute no terminal do seu projeto consumidor:
```bash
# Instala a biblioteca eli-vue diretamente do repositório Git
bun add git+https://git2.idz.one/publico/vue-componentes.git
# Dependências (Peer Dependencies)
bun add vue vuetify lucide-vue-next leaflet
bun add -d @types/leaflet
```
> **💡 Como o pacote é registrado no `package.json`**:
> O Bun clona o repositório `vue-componentes.git`, identifica o nome `"eli-vue"` no `package.json` e registra a dependência automaticamente como:
> ```json
> "dependencies": {
> "eli-vue": "git+https://git2.idz.one/publico/vue-componentes.git"
> }
> ```
>
> ⚠️ **Importante**: Não adicione o nome `eli-vue` como parâmetro extra no comando (isto é, evite `bun add git+... eli-vue`). O Bun trataria `eli-vue` como um segundo pacote a ser buscado no registro público do npm, gerando erro `404 Not Found`.
Certifique-se de que seu projeto esteja configurado com Vuetify 3.
> **Nota**: O CSS do Leaflet é incluído automaticamente no bundle do eli-vue. Não é necessário importar `leaflet/dist/leaflet.css` manualmente.
---
## 🎨 Temas da Aplicação (`definirTemaAplicacao`)
O `eli-vue` suporta temas institucionais prontos para os produtos do ecossistema:
- **`"verde"`** (padrão): Módulos **e-licencie** (paleta institucional Verde Floresta `#688C00`).
- **`"azul"`**: Módulos **Betha Meio Ambiente** (paleta Azul Betha `#007BFF` / `#0069D9`).
### Uso no `main.ts`
```typescript
import { createApp } from "vue"
import EliVue, { definirTemaAplicacao, obterCoresVuetifyTema } from "eli-vue"
import { createVuetify } from "vuetify"
import App from "./App.vue"
// 1. Define o tema visual do Design System (tokens CSS no :root)
definirTemaAplicacao("azul") // ou "verde"
// 2. (Opcional) Configurar cores correspondentes no Vuetify 3
const vuetify = createVuetify({
theme: {
defaultTheme: "azul",
themes: {
azul: {
dark: false,
colors: {
...obterCoresVuetifyTema("azul"),
},
},
verde: {
dark: false,
colors: {
...obterCoresVuetifyTema("verde"),
},
},
},
},
})
const app = createApp(App)
app.use(vuetify)
app.use(EliVue)
app.mount("#app")
```
---
## 📦 Referência de Componentes
### 1. EliTabela (Tabela de Dados)
O componente `EliTabela` é uma tabela poderosa com paginação no servidor (server-side), busca integrada, ordenação e menus de ação.
#### API
- **Prop**: `tabela` (Obrigatório)
- **Tipo**: `tipoEliTabelaConsulta<T>`
#### Interface de Configuração (`tipoEliTabelaConsulta`)
```typescript
// De: eli-vue/src/componentes/EliTabela/types-eli-tabela.ts
export type tipoEliTabelaConsulta<T> = {
// Identificador único para a tabela (usado para persistência local de visibilidade das colunas)
nome: string;
// Função para buscar dados. DEVE retornar um objeto de resposta padrão.
consulta: (params: parametrosConsulta<T>) => Promise<tipoResposta<tipoEliConsultaPaginada<T>>>;
// Definições das colunas
colunas: tipoEliColuna<T>[];
// Opções de UI
mostrarCaixaDeBusca?: boolean;
registros_por_consulta?: number; // padrão: 10
maximo_botoes_paginacao?: number; // padrão: 7
mensagemVazio?: string;
// Ações
acoesLinha?: tipoEliTabelaAcao<T>[]; // Ações para cada linha (menu de contexto)
acoesTabela?: Array<{ // Ações globais (superior/inferior)
// Ações da Tabela (Botões Globais)
acoesTabela?: Array<{
/**
* Posição do botão:
* - "superior": Ao lado da caixa de busca (canto superior direito).
* - "inferior": No rodapé, alinhado à esquerda (canto inferior esquerdo).
*/
posicao: "superior" | "inferior";
/** Rótulo do botão */
rotulo: string;
/** Ícone (Lucide) */
icone?: LucideIcon;
/** Cor do botão (ex: "primary", "success", "error") */
cor?: string;
/**
* Função executada ao clicar.
* Recebe um objeto contendo:
* - Parâmetros atuais da consulta (offSet, limit, filtros, etc.)
* - Helper `atualizarConsulta()`: Promise<void> para recarregar a tabela.
*/
acao: (params: parametrosConsulta<T> & { atualizarConsulta: () => Promise<void> }) => void;
}>;
// Definição de Filtro Avançado
filtroAvancado?: Array<{
chave: string;
rotulo: string;
/** Função que gera o filtro com base no valor recebido do input */
filtro: (valor: unknown) => tipoFiltro<T>;
/** Definição do componente de entrada, ex: ["texto", { ... }] */
entrada: any;
}>;
};
```
#### Definindo Colunas (helper `celulaTabela`)
Use o helper `celulaTabela` para criar definições de células com segurança de tipo.
```typescript
// Tipos de células disponíveis e seus dados:
// "textoSimples": { texto: string; acao?: () => void }
// "textoTruncado": { texto: string; acao?: () => void }
// "numero": { numero: number; prefixo?: string; sufixo?: string; acao?: () => void }
// "tags": { opcoes: Array<{ rotulo: string; cor?: string; icone?: LucideIcon; acao?: () => void }> }
// "data": { valor: string; formato: "data" | "data_hora" | "relativo"; acao?: () => void }
// "badge": { texto: string; cor: string }
{
rotulo: "Nome",
celula: (row) => celulaTabela("textoSimples", { texto: row.nome }),
visivel: true
}
```
#### Exemplo de Uso com Filtros Avançados
```vue
<script setup lang="ts">
import { EliTabela, celulaTabela, criarFiltro26 } from "eli-vue";
import type { tipoEliTabelaConsulta } from "eli-vue";
import { UsuarioService } from "@/services/UsuarioService";
import { BadgeCheck, Pencil, Plus } from "lucide-vue-next";
type Usuario = { id: number; nome: string; email: string; ativo: boolean; created_at: string };
const tabelaConfig: tipoEliTabelaConsulta<Usuario> = {
nome: "tabela-usuarios",
mostrarCaixaDeBusca: true,
registros_por_consulta: 10,
colunas: [
{
rotulo: "Nome",
celula: (usuario) => celulaTabela("textoSimples", { texto: usuario.nome }),
visivel: true,
coluna_ordem: "nome"
},
{
rotulo: "Status",
visivel: true,
celula: (usuario) => celulaTabela("tags", {
opcoes: [
usuario.ativo
? { rotulo: "Ativo", cor: "success", icone: BadgeCheck }
: { rotulo: "Inativo", cor: "error" }
]
})
},
{
rotulo: "Criado em",
celula: (usuario) => celulaTabela("data", { valor: usuario.created_at, formato: "data_hora" }),
visivel: true
}
],
// Configuração de filtros avançados
filtroAvancado: [
{
rotulo: "Nome",
chave: "nome",
// Função que retorna o objeto de filtro estruturado
filtro: (valor: unknown) => criarFiltro26({ nome: { like: valor as string } }),
// Definição do input: ["tipo", opcoes]
entrada: ["texto", { rotulo: "Nome do usuário" }] as any
},
{
rotulo: "Ativo?",
chave: "ativo",
filtro: (valor: unknown) => criarFiltro26({ ativo: { "=": valor } }),
entrada: ["selecao", {
rotulo: "Status",
itens: () => [
{ chave: "true", rotulo: "Ativo" },
{ chave: "false", rotulo: "Inativo" }
]
}] as any
}
],
acoesTabela: [
{
rotulo: "Novo Usuário",
icone: Plus,
posicao: "superior",
cor: "primary",
// O callback recebe: offSet, limit, filtros, e o helper 'atualizarConsulta'
acao: async ({ atualizarConsulta }) => {
// Exemplo: Abrir modal de criação e depois atualizar a tabela
await openCreateUserModal();
await atualizarConsulta();
}
},
{
rotulo: "Exportar CSV",
icone: Download,
posicao: "inferior",
cor: "success",
acao: (params) => {
// 'params' contém os filtros atuais aplicados na tabela
UsuarioService.exportarCsv(params);
}
}
],
consulta: async (params) => {
// params contém: offSet, limit, texto_busca, coluna_ordem, direcao_ordem, filtros
return await UsuarioService.listar(params);
},
acoesLinha: [
{
rotulo: "Editar",
icone: Pencil,
cor: "primary",
acao: (usuario) => console.log("Editar", usuario)
}
]
};
</script>
<template>
<EliTabela :tabela="tabelaConfig" />
</template>
```
---
### 2. EliEntrada (Inputs)
Um conjunto de wrappers padronizados em torno dos campos do Vuetify.
#### Variantes
- `EliEntradaTexto`: Texto, Email, URL, CPF, CNPJ, CEP.
- `EliEntradaNumero`: Inteiros, Decimais, Moeda.
- `EliEntradaDataHora`: Data, DataHora.
- `EliEntradaSelecao`: Select/Dropdown (pode ser assíncrono).
- `EliEntradaSelecaoMultipla`: Multi-select (pode ser assíncrono).
- `EliEntradaParagrafo`: Textarea.
- `EliEntradaCoordenadas`: Mapa Leaflet interativo com inputs de lat/lng.
#### API Comum
Todas as entradas aceitam duas props principais:
1. `value`: O binding v-model.
2. `opcoes`: Um objeto de configuração específico para o tipo de entrada.
#### Objetos de Configuração (`tiposEntradas.ts`)
**EliEntradaTexto**
```typescript
opcoes: {
rotulo: string;
placeholder?: string;
limiteCaracteres?: number;
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep";
}
```
**EliEntradaNumero**
```typescript
opcoes: {
rotulo: string;
precisao?: number; // 0=inteiro (padrão se null), 0.1=1 decimal, 0.01=2 decimais. Se undefined/null, assume decimal livre.
prefixo?: string; // ex: "R$"
sufixo?: string; // ex: "kg"
}
```
**EliEntradaDataHora**
```typescript
opcoes: {
rotulo: string;
modo?: "data" | "dataHora"; // padrão: "dataHora"
min?: string; // String ISO
max?: string; // String ISO
}
```
**EliEntradaSelecao**
```typescript
opcoes: {
rotulo: string;
// Função que retorna itens ou promise de itens.
// IMPORTANTE: Itens devem ser objetos { chave: string, rotulo: string }
itens: () => Array<{ chave: string; rotulo: string }> | Promise<Array<{ chave: string; rotulo: string }>>;
// Lista de chaves que devem aparecer desativadas no menu.
// Os itens ficam visíveis mas não podem ser selecionados.
desativado?: string[];
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
densidade?: "default" | "comfortable" | "compact";
variante?: "outlined" | "filled" | "underlined" | "solo" | "plain";
}
```
**EliEntradaSelecaoMultipla**
```typescript
// value: string[] | null | undefined
opcoes: {
rotulo: string;
// Mesma API de itens que EliEntradaSelecao
itens: () => Array<{ chave: string; rotulo: string }> | Promise<Array<{ chave: string; rotulo: string }>>;
// Lista de chaves que devem aparecer desativadas no menu.
desativado?: string[];
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
densidade?: "default" | "comfortable" | "compact";
variante?: "outlined" | "filled" | "underlined" | "solo" | "plain";
}
```
> **Itens desativados**: a prop `desativado` recebe um array de `chave`s. Os itens correspondentes aparecem no menu mas não podem ser clicados (estilo esmaecido do Vuetify).
>
> ```typescript
> // Exemplo: desativar opções específicas dinamicamente
> opcoes: {
> rotulo: "Status",
> itens: async () => [
> { chave: "ativo", rotulo: "Ativo" },
> { chave: "inativo", rotulo: "Inativo" },
> { chave: "suspenso", rotulo: "Suspenso" },
> ],
> desativado: ["suspenso"], // "Suspenso" aparece mas não é selecionável
> }
> ```
**EliEntradaCoordenadas**
```typescript
// value: { formato: "decimal", datum: "EPSG:4326", latitude: number, longitude: number }
opcoes: {
rotulo: string;
zoom?: number; // Nível de zoom inicial (padrão: 4)
alturaMapa?: number; // Altura do mapa em pixels (padrão: 300)
apenasLeitura?: boolean; // Desabilita clique e arraste no mapa
}
```
> **Importante**: Requer `import "leaflet/dist/leaflet.css"` no entry point do app consumidor.
> O componente inclui: mapa Leaflet interativo, inputs editáveis de lat/lng (aceita vírgula BR),
> marcador arrastável, botão "Usar minha localização" (GPS + GeoIP fallback),
> e seletor de camadas (Mapa / Satélite).
#### Exemplo de Uso
```vue
<script setup lang="ts">
import { ref } from "vue";
import { EliEntradaTexto, EliEntradaNumero, EliEntradaDataHora } from "eli-vue";
const nome = ref("");
const salario = ref<number | null>(null);
const nascimento = ref<string | null>(null); // ISO string
</script>
<template>
<EliEntradaTexto
:value="nome"
@update:value="nome = $event"
:opcoes="{ rotulo: 'Nome Completo', formato: 'texto' }"
/>
<EliEntradaNumero
:value="salario"
@update:value="salario = $event"
:opcoes="{ rotulo: 'Salário', prefixo: 'R$', precisao: 0.01 }"
/>
<!-- Exibe como data local, vincula como string ISO -->
<EliEntradaDataHora
:value="nascimento"
@update:value="nascimento = $event"
:opcoes="{ rotulo: 'Data de Nascimento', modo: 'data' }"
/>
</template>
```
---
### 3. EliBotao
Wrapper simples para `v-btn`.
- **Props**: `color`, `variant` (elevated, flat, text, etc.), `size`, `loading`, `disabled`.
- **Slots**: slot padrão para o conteúdo do botão.
```vue
<EliBotao color="primary" @click="salvar">
Salvar
</EliBotao>
```
---
### 4. EliCartao & EliBadge
**EliCartao**
Cartão padronizado para itens de domínio.
- **Props**: `titulo`, `status` (tipo `CartaoStatus`: "novo" | "rascunho" | "vendido" | "cancelado"), `variant`.
- **Slots**: `default` (conteúdo), `acoes` (ações de rodapé).
**EliBadge**
Badge aprimorado com raios.
- **Props**: `badge` (conteúdo), `color`, `radius` ("suave" | "pill").
---
## 🔧 Solução de Problemas e Dicas
### 1. "Failed to resolve component"
- Certifique-se de que `app.use(EliVue)` foi chamado no `main.ts`.
- Se importar diretamente, garanta importações padrão: `import { EliTabela } from 'eli-vue'`.
### 2. Problemas de Estilo
- **Injeção de CSS**: O `eli-vue` injeta CSS automaticamente. Verifique sua CSP (Política de Segurança de Conteúdo) se os estilos estiverem bloqueados.
- **Vuetify**: Certifique-se de que o Vuetify 3 está corretamente instalado e os estilos (`vuetify/styles`) estão importados no seu arquivo de entrada principal.
### 3. Erros de TypeScript no `celulaTabela`
- Garanta que o segundo argumento corresponda ao esquema específico do tipo de célula.
- Exemplo: `celulaTabela("textoSimples", { texto: ... })` funciona, mas `celulaTabela("textoSimples", { numero: ... })` falhará.
### 4. Ícones não aparecem
- Passe o **objeto do componente** (ex: importado de `lucide-vue-next`), não o nome em string.
- Exemplo: `icone: Pencil` (Correto) vs `icone: "pencil"` (Incorreto).
### 5. Valores do `EliEntrada`
- `EliEntradaTexto` com `formato='cpfCnpj'` emite a string **formatada** (ex: "123.456.789-00").
- `EliEntradaNumero` emite um `number` ou `null`.
- `EliEntradaDataHora` trabalha com **Strings ISO** (ex: "2023-01-01T00:00:00Z"). A exibição é localizada, mas o valor do modelo é sempre ISO.