Compare commits

...

10 commits

Author SHA1 Message Date
8c5a31ef30 build 2026-01-29 16:10:58 -03:00
1e3c4026e8 aplicado Filtro 2026-01-29 15:33:42 -03:00
e7357e064a bkp 2026-01-29 13:38:24 -03:00
0144788548 Criado todo para filtro avançado 2026-01-29 11:41:45 -03:00
27c9e4d5e2 rafatoração de componentes de entrada 2026-01-29 11:27:08 -03:00
6aedf2469f bkp 2026-01-29 10:51:13 -03:00
de7c19be24 emplementado entrada numero e entrada texto 2026-01-29 10:35:35 -03:00
fa1f93aedc reorganização de arquivos 2026-01-29 09:21:31 -03:00
317b0b3b3e resolvido erro de ação de células 2026-01-29 09:20:39 -03:00
4fd142ee70 bkp 2026-01-29 08:49:40 -03:00
108 changed files with 5995 additions and 3426 deletions

39
.agent
View file

@ -40,6 +40,27 @@ Construir um Design System de componentes em **Vue 3** para reutilização em m
--- ---
## Convenção atual de entradas (IMPORTANTE)
O componente **`EliInput` foi removido**. O padrão atual é a família **`EliEntrada*`**:
- `EliEntradaTexto`
- `EliEntradaNumero`
- `EliEntradaDataHora`
E o contrato padrão para entradas é:
- prop `value`
- evento `update:value`
- prop obrigatória `opcoes` (contém `rotulo` e outras opções)
Exemplo:
```vue
<EliEntradaTexto v-model:value="nome" :opcoes="{ rotulo: 'Nome' }" />
```
---
## Estrutura obrigatória do repositório ## Estrutura obrigatória do repositório
- Cada componente deve possuir **sua própria pasta** em `src/componentes/` - Cada componente deve possuir **sua própria pasta** em `src/componentes/`
- Dentro de cada pasta do componente: - Dentro de cada pasta do componente:
@ -162,6 +183,24 @@ Evitar comentários óbvios (“isso é um botão”).
--- ---
## Convenção atual de EliTabela (IMPORTANTE)
### Filtro avançado
O filtro avançado da `EliTabela` é configurado via `tabela.filtroAvancado`.
Regras:
- O **operador é travado na definição** (o usuário não escolhe operador)
- Cada filtro pode ser usado **no máximo 1 vez**
- UI: modal mostra **apenas os componentes de entrada** definidos no filtro
- Persistência: salva em `localStorage` apenas `{ coluna, valor }[]` por `tabela.nome`
Se você for evoluir isso para backend:
- usar `parametrosConsulta.filtros` (`tipoFiltro[]`) no `tabela.consulta`
- manter compatibilidade com simulação local (quando necessário)
---
## Publicação do pacote (npm) ## Publicação do pacote (npm)
### Regra de publicação (sem usar `package.json.files`) ### Regra de publicação (sem usar `package.json.files`)

114
IA.md
View file

@ -61,7 +61,15 @@ createApp(App)
### 2) Importação direta (quando não quiser plugin) ### 2) Importação direta (quando não quiser plugin)
```ts ```ts
import { EliBotao, EliInput, EliBadge, EliCartao, EliDataHora } from "eli-vue"; import {
EliBotao,
EliBadge,
EliCartao,
EliTabela,
EliEntradaTexto,
EliEntradaNumero,
EliEntradaDataHora,
} from "eli-vue";
``` ```
> Observação: ainda pode ser necessário importar o CSS do pacote: > Observação: ainda pode ser necessário importar o CSS do pacote:
@ -82,11 +90,18 @@ import "eli-vue/dist/eli-vue.css";
</template> </template>
``` ```
### Input com v-model ### Entradas (EliEntrada*) com v-model
O `eli-vue` usa uma família de componentes `EliEntrada*` (em vez do antigo `EliInput`).
#### Texto
```vue ```vue
<template> <template>
<EliInput v-model="nome" label="Nome" placeholder="Digite seu nome" /> <EliEntradaTexto
v-model:value="nome"
:opcoes="{ rotulo: 'Nome', placeholder: 'Digite seu nome' }"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -94,33 +109,37 @@ import { defineComponent, ref } from "vue";
export default defineComponent({ export default defineComponent({
setup() { setup() {
const nome = ref(""); const nome = ref<string | null>("");
function salvar() { return { nome };
// ...
}
return { nome, salvar };
}, },
}); });
</script> </script>
``` ```
### Input de porcentagem #### Texto com formato/máscara
Quando precisar de um campo numérico com sufixo `%`, use `type="porcentagem"`. > Regra importante: o `value` emitido é **sempre o texto formatado** (igual ao que aparece no input).
```vue ```vue
<template> <template>
<EliInput v-model="taxa" type="porcentagem" label="Taxa" placeholder="0,00" /> <EliEntradaTexto
v-model:value="documento"
:opcoes="{ rotulo: 'CPF/CNPJ', formato: 'cpfCnpj' }"
/>
<EliEntradaTexto
v-model:value="telefone"
:opcoes="{ rotulo: 'Telefone', formato: 'telefone' }"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from "vue"; import { defineComponent, ref } from "vue";
export default defineComponent({ export default defineComponent({
setup() { setup() {
const taxa = ref(""); const documento = ref<string | null>("");
return { taxa }; const telefone = ref<string | null>("");
return { documento, telefone };
}, },
}); });
</script> </script>
@ -131,10 +150,10 @@ export default defineComponent({
```vue ```vue
<template> <template>
<!-- Valor chega do backend em ISO 8601 (UTC/offset), e o componente exibe em horário local --> <!-- Valor chega do backend em ISO 8601 (UTC/offset), e o componente exibe em horário local -->
<EliDataHora v-model="dataHora" rotulo="Agendamento" /> <EliEntradaDataHora v-model:value="dataHora" :opcoes="{ rotulo: 'Agendamento' }" />
<!-- Somente data --> <!-- Somente data -->
<EliDataHora v-model="data" modo="data" rotulo="Nascimento" /> <EliEntradaDataHora v-model:value="data" :opcoes="{ rotulo: 'Nascimento', modo: 'data' }" />
</template> </template>
<script lang="ts"> <script lang="ts">
@ -152,6 +171,67 @@ export default defineComponent({
--- ---
## EliTabela (com filtro avançado)
O componente `EliTabela` suporta:
- ordenação
- paginação
- caixa de busca
- **filtro avançado (modal)**
### Contrato da tabela (resumo)
O tipo principal é `EliTabelaConsulta<T>` (genérico), exportado de `eli-vue`.
O `filtroAvancado` é uma lista de filtros pré-definidos (o usuário só escolhe quais usar e informa valores):
```ts
filtroAvancado?: {
rotulo: string
coluna: keyof T
operador: string // ex.: "like", "=", ">", "in", "isNull"
entrada: ["texto" | "numero" | "dataHora", { rotulo: string; ... }]
}[]
```
### Exemplo mínimo
```ts
import { EliTabela, celulaTabela } from "eli-vue";
import type { EliTabelaConsulta } from "eli-vue";
import type { ComponenteEntrada } from "eli-vue/dist/types/componentes/EliEntrada/tiposEntradas";
type Linha = { nome: string; documento: string; email: string };
const tabela: EliTabelaConsulta<Linha> = {
nome: "Exemplo",
mostrarCaixaDeBusca: true,
registros_por_consulta: 10,
colunas: [
{ rotulo: "Nome", celula: (l) => celulaTabela("textoSimples", { texto: l.nome }), visivel: true },
],
filtroAvancado: [
{
rotulo: "Documento",
coluna: "documento",
operador: "like",
entrada: ["texto", { rotulo: "Documento", formato: "cpfCnpj" }] as unknown as ComponenteEntrada,
},
],
consulta: async () => ({
cod: 0,
eCerto: true,
eErro: false,
mensagem: undefined,
valor: { quantidade: 0, valores: [] },
}),
};
```
> Observação: em modo simulação/local, a tabela pode buscar uma lista completa e aplicar filtro/paginação localmente.
---
## Troubleshooting (para IAs) ## Troubleshooting (para IAs)
### 1) “Failed to resolve component” ### 1) “Failed to resolve component”

View file

@ -35,21 +35,26 @@ src/
EliBotao.vue EliBotao.vue
index.ts index.ts
README.md README.md
campo/ cartao/
EliInput.vue EliCartao.vue
index.ts index.ts
README.md README.md
indicador/ indicador/
EliBadge.vue EliBadge.vue
index.ts index.ts
README.md README.md
data_hora/ EliEntrada/
EliDataHora.vue EliEntradaTexto.vue
EliEntradaNumero.vue
EliEntradaDataHora.vue
index.ts
README.md
EliTabela/
EliTabela.vue
index.ts index.ts
README.md README.md
tipos/ tipos/
botao.ts botao.ts
campo.ts
indicador.ts indicador.ts
index.ts index.ts
playground/ playground/
@ -60,7 +65,7 @@ src/
### Convenções (nomenclatura) ### Convenções (nomenclatura)
- Componentes usam **prefixo `Eli`** (ex.: `EliBotao`, `EliInput`). - Componentes usam **prefixo `Eli`** (ex.: `EliBotao`, `EliEntradaTexto`).
- Pastas preferem **português** (ex.: `src/componentes/botao`, `src/componentes/campo`). - Pastas preferem **português** (ex.: `src/componentes/botao`, `src/componentes/campo`).
- Tipos compartilhados ficam em `src/tipos/`. - Tipos compartilhados ficam em `src/tipos/`.
- Sem TSX; padrão SFC: `<template>` + `<script lang="ts">` + `defineComponent`. - Sem TSX; padrão SFC: `<template>` + `<script lang="ts">` + `defineComponent`.
@ -128,7 +133,15 @@ createApp(App)
### 2) Importação direta de componentes ### 2) Importação direta de componentes
```ts ```ts
import { EliBotao, EliInput, EliBadge, EliDataHora } from "eli-vue"; import {
EliBotao,
EliBadge,
EliCartao,
EliTabela,
EliEntradaTexto,
EliEntradaNumero,
EliEntradaDataHora,
} from "eli-vue";
``` ```
## Exemplos rápidos de uso ## Exemplos rápidos de uso
@ -141,11 +154,14 @@ import { EliBotao, EliInput, EliBadge, EliDataHora } from "eli-vue";
</template> </template>
``` ```
### EliInput (v-model) ### Entradas (EliEntrada*) com v-model
```vue ```vue
<template> <template>
<EliInput v-model="nome" label="Nome" placeholder="Digite seu nome" /> <EliEntradaTexto
v-model:value="nome"
:opcoes="{ rotulo: 'Nome', placeholder: 'Digite seu nome' }"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -153,20 +169,21 @@ import { defineComponent, ref } from "vue";
export default defineComponent({ export default defineComponent({
setup() { setup() {
const nome = ref(""); const nome = ref<string | null>("");
return { nome }; return { nome };
}, },
}); });
</script> </script>
``` ```
### EliInput (porcentagem) ### EliEntradaNumero (exemplo)
Use `type="porcentagem"` quando precisar de um campo numérico com sufixo `%` embutido.
```vue ```vue
<template> <template>
<EliInput v-model="taxa" type="porcentagem" label="Taxa" placeholder="0,00" /> <EliEntradaNumero
v-model:value="taxa"
:opcoes="{ rotulo: 'Taxa', placeholder: '0,00' }"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -174,7 +191,7 @@ import { defineComponent, ref } from "vue";
export default defineComponent({ export default defineComponent({
setup() { setup() {
const taxa = ref(""); const taxa = ref<number | null>(null);
return { taxa }; return { taxa };
}, },
}); });
@ -191,7 +208,7 @@ export default defineComponent({
</template> </template>
``` ```
### EliDataHora ### EliEntradaDataHora
> Entrada/saída sempre em **ISO 8601**. > Entrada/saída sempre em **ISO 8601**.
> - Aceita UTC absoluto (`Z`) ou com offset. > - Aceita UTC absoluto (`Z`) ou com offset.
@ -199,16 +216,19 @@ export default defineComponent({
```vue ```vue
<template> <template>
<EliDataHora v-model="agendamento" rotulo="Agendamento" /> <EliEntradaDataHora v-model:value="agendamento" :opcoes="{ rotulo: 'Agendamento' }" />
<EliDataHora v-model="nascimento" modo="data" rotulo="Nascimento" /> <EliEntradaDataHora
v-model:value="nascimento"
:opcoes="{ rotulo: 'Nascimento', modo: 'data' }"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from "vue"; import { defineComponent, ref } from "vue";
import { EliDataHora } from "eli-vue"; import { EliEntradaDataHora } from "eli-vue";
export default defineComponent({ export default defineComponent({
components: { EliDataHora }, components: { EliEntradaDataHora },
setup() { setup() {
const agendamento = ref<string | null>("2026-01-09T16:15:00Z"); const agendamento = ref<string | null>("2026-01-09T16:15:00Z");
const nascimento = ref<string | null>("2026-01-09T00:00:00-03:00"); const nascimento = ref<string | null>("2026-01-09T00:00:00-03:00");
@ -253,11 +273,9 @@ Exemplo: um **pipeline** em colunas (estilo Trello/Kanban) com cards de oportuni
<v-container class="py-6"> <v-container class="py-6">
<div class="toolbar"> <div class="toolbar">
<h2 class="text-h6">Pipeline de Oportunidades</h2> <h2 class="text-h6">Pipeline de Oportunidades</h2>
<EliInput <EliEntradaTexto
v-model="filtro" v-model:value="filtro"
label="Buscar" :opcoes="{ rotulo: 'Buscar', placeholder: 'Cliente, proposta, valor...' }"
placeholder="Cliente, proposta, valor..."
density="compact"
/> />
<EliBotao @click="criarOportunidade">Nova oportunidade</EliBotao> <EliBotao @click="criarOportunidade">Nova oportunidade</EliBotao>
</div> </div>
@ -313,7 +331,7 @@ Exemplo: um **pipeline** em colunas (estilo Trello/Kanban) com cards de oportuni
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from "vue"; import { defineComponent, ref } from "vue";
import { EliBadge, EliBotao, EliInput } from "eli-vue"; import { EliBadge, EliBotao, EliEntradaTexto } from "eli-vue";
type Oportunidade = { type Oportunidade = {
id: string; id: string;
@ -332,7 +350,7 @@ type Coluna = {
export default defineComponent({ export default defineComponent({
name: "PipelineExemplo", name: "PipelineExemplo",
components: { EliBadge, EliBotao, EliInput }, components: { EliBadge, EliBotao, EliEntradaTexto },
setup() { setup() {
const filtro = ref(""); const filtro = ref("");
@ -467,6 +485,9 @@ pnpm run build
Gera `dist/` (artefatos de build) e `dist/types` (declarações `.d.ts`). Gera `dist/` (artefatos de build) e `dist/types` (declarações `.d.ts`).
> Observação importante: este repositório roda `npm version patch --no-git-tag-version` no `prebuild`.
> Ou seja, ao rodar `pnpm run build` a versão do `package.json` é incrementada automaticamente.
## Guia rápido para IAs (antes de codar) ## Guia rápido para IAs (antes de codar)
1) **Leia** `.agent` e este README. 1) **Leia** `.agent` e este README.

2
dist/eli-vue.css vendored

File diff suppressed because one or more lines are too long

3257
dist/eli-vue.es.js vendored

File diff suppressed because it is too large Load diff

38
dist/eli-vue.umd.js vendored

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,231 @@
import { PropType } from "vue";
import type { CampoDensidade, CampoVariante } from "../../tipos";
import type { PadroesEntradas } from "./tiposEntradas";
type EntradaDataHora = PadroesEntradas["dataHora"];
type PropsAntigas = {
modo?: "data" | "dataHora";
rotulo?: string;
placeholder?: string;
desabilitado?: boolean;
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
densidade?: CampoDensidade;
variante?: CampoVariante;
min?: string;
max?: string;
};
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
value: {
type: PropType<EntradaDataHora["value"]>;
default: undefined;
};
opcoes: {
type: PropType<EntradaDataHora["opcoes"]>;
required: false;
default: undefined;
};
modelValue: {
type: PropType<string | null>;
default: null;
};
modo: {
type: PropType<PropsAntigas["modo"]>;
default: undefined;
};
rotulo: {
type: StringConstructor;
default: undefined;
};
placeholder: {
type: StringConstructor;
default: undefined;
};
desabilitado: {
type: BooleanConstructor;
default: undefined;
};
limpavel: {
type: BooleanConstructor;
default: undefined;
};
erro: {
type: BooleanConstructor;
default: undefined;
};
mensagensErro: {
type: PropType<string | string[]>;
default: undefined;
};
dica: {
type: StringConstructor;
default: undefined;
};
dicaPersistente: {
type: BooleanConstructor;
default: undefined;
};
densidade: {
type: PropType<CampoDensidade>;
default: undefined;
};
variante: {
type: PropType<CampoVariante>;
default: undefined;
};
min: {
type: PropType<string | undefined>;
default: undefined;
};
max: {
type: PropType<string | undefined>;
default: undefined;
};
}>, {
attrs: {
[x: string]: unknown;
};
valor: import("vue").WritableComputedRef<string, string>;
tipoInput: import("vue").ComputedRef<"date" | "datetime-local">;
minLocal: import("vue").ComputedRef<string | undefined>;
maxLocal: import("vue").ComputedRef<string | undefined>;
opcoesEfetivas: import("vue").ComputedRef<{
rotulo: string;
placeholder?: string;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../tipos").CampoDensidade;
variante?: import("../../tipos").CampoVariante;
}>;
desabilitadoEfetivo: import("vue").ComputedRef<boolean>;
emitCompatFocus: () => void;
emitCompatBlur: () => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
"update:value": (_v: string | null) => true;
input: (_v: string | null) => true;
change: (_v: string | null) => true;
"update:modelValue": (_v: string | null) => true;
alterar: (_v: string | null) => true;
foco: () => true;
desfoco: () => true;
focus: () => true;
blur: () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
value: {
type: PropType<EntradaDataHora["value"]>;
default: undefined;
};
opcoes: {
type: PropType<EntradaDataHora["opcoes"]>;
required: false;
default: undefined;
};
modelValue: {
type: PropType<string | null>;
default: null;
};
modo: {
type: PropType<PropsAntigas["modo"]>;
default: undefined;
};
rotulo: {
type: StringConstructor;
default: undefined;
};
placeholder: {
type: StringConstructor;
default: undefined;
};
desabilitado: {
type: BooleanConstructor;
default: undefined;
};
limpavel: {
type: BooleanConstructor;
default: undefined;
};
erro: {
type: BooleanConstructor;
default: undefined;
};
mensagensErro: {
type: PropType<string | string[]>;
default: undefined;
};
dica: {
type: StringConstructor;
default: undefined;
};
dicaPersistente: {
type: BooleanConstructor;
default: undefined;
};
densidade: {
type: PropType<CampoDensidade>;
default: undefined;
};
variante: {
type: PropType<CampoVariante>;
default: undefined;
};
min: {
type: PropType<string | undefined>;
default: undefined;
};
max: {
type: PropType<string | undefined>;
default: undefined;
};
}>> & Readonly<{
"onUpdate:value"?: ((_v: string | null) => any) | undefined;
onInput?: ((_v: string | null) => any) | undefined;
onChange?: ((_v: string | null) => any) | undefined;
onFocus?: (() => any) | undefined;
onBlur?: (() => any) | undefined;
onAlterar?: ((_v: string | null) => any) | undefined;
"onUpdate:modelValue"?: ((_v: string | null) => any) | undefined;
onFoco?: (() => any) | undefined;
onDesfoco?: (() => any) | undefined;
}>, {
modo: "data" | "dataHora" | undefined;
limpavel: boolean;
erro: boolean;
mensagensErro: string | string[];
dica: string;
dicaPersistente: boolean;
min: string | undefined;
max: string | undefined;
densidade: CampoDensidade;
variante: CampoVariante;
opcoes: {
rotulo: string;
placeholder?: string;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../tipos").CampoDensidade;
variante?: import("../../tipos").CampoVariante;
};
value: string | null | undefined;
placeholder: string;
rotulo: string;
modelValue: string | null;
desabilitado: boolean;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
declare const _default: typeof __VLS_export;
export default _default;

View file

@ -0,0 +1,49 @@
import { PropType } from "vue";
import type { PadroesEntradas } from "./tiposEntradas";
type EntradaNumero = PadroesEntradas["numero"];
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
/** Interface padrão (EliEntrada): value + opcoes. */
value: {
type: PropType<EntradaNumero["value"]>;
default: undefined;
};
opcoes: {
type: PropType<EntradaNumero["opcoes"]>;
required: true;
};
}>, {
attrs: {
[x: string]: unknown;
};
emit: ((event: "update:value", _v: number | null | undefined) => void) & ((event: "input", _v: number | null | undefined) => void) & ((event: "change", _v: number | null | undefined) => void) & ((event: "focus") => void) & ((event: "blur") => void);
displayValue: import("vue").Ref<string, string>;
isInteiro: import("vue").ComputedRef<boolean>;
onUpdateModelValue: (texto: string) => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
"update:value": (_v: EntradaNumero["value"]) => true;
/** Compat Vue2 (v-model padrão: value + input) */
input: (_v: EntradaNumero["value"]) => true;
change: (_v: EntradaNumero["value"]) => true;
focus: () => true;
blur: () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
/** Interface padrão (EliEntrada): value + opcoes. */
value: {
type: PropType<EntradaNumero["value"]>;
default: undefined;
};
opcoes: {
type: PropType<EntradaNumero["opcoes"]>;
required: true;
};
}>> & Readonly<{
"onUpdate:value"?: ((_v: number | null | undefined) => any) | undefined;
onInput?: ((_v: number | null | undefined) => any) | undefined;
onChange?: ((_v: number | null | undefined) => any) | undefined;
onFocus?: (() => any) | undefined;
onBlur?: (() => any) | undefined;
}>, {
value: number | null | undefined;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
declare const _default: typeof __VLS_export;
export default _default;

View file

@ -0,0 +1,50 @@
import { PropType } from "vue";
import type { PadroesEntradas } from "./tiposEntradas";
type EntradaTexto = PadroesEntradas["texto"];
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
/** Interface padrão (EliEntrada): value + opcoes. */
value: {
type: PropType<EntradaTexto["value"]>;
default: undefined;
};
opcoes: {
type: PropType<EntradaTexto["opcoes"]>;
required: true;
};
}>, {
attrs: {
[x: string]: unknown;
};
emit: ((event: "update:value", _v: string | null | undefined) => void) & ((event: "input", _v: string | null | undefined) => void) & ((event: "change", _v: string | null | undefined) => void) & ((event: "focus") => void) & ((event: "blur") => void);
localValue: import("vue").WritableComputedRef<string | null | undefined, string | null | undefined>;
inputHtmlType: import("vue").ComputedRef<"text" | "email" | "url">;
inputMode: import("vue").ComputedRef<string | undefined>;
onInput: (e: Event) => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
"update:value": (_v: EntradaTexto["value"]) => true;
/** Compat Vue2 (v-model padrão: value + input) */
input: (_v: EntradaTexto["value"]) => true;
change: (_v: EntradaTexto["value"]) => true;
focus: () => true;
blur: () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
/** Interface padrão (EliEntrada): value + opcoes. */
value: {
type: PropType<EntradaTexto["value"]>;
default: undefined;
};
opcoes: {
type: PropType<EntradaTexto["opcoes"]>;
required: true;
};
}>> & Readonly<{
"onUpdate:value"?: ((_v: string | null | undefined) => any) | undefined;
onInput?: ((_v: string | null | undefined) => any) | undefined;
onChange?: ((_v: string | null | undefined) => any) | undefined;
onFocus?: (() => any) | undefined;
onBlur?: (() => any) | undefined;
}>, {
value: string | null | undefined;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
declare const _default: typeof __VLS_export;
export default _default;

View file

@ -0,0 +1,5 @@
import EliEntradaTexto from "./EliEntradaTexto.vue";
import EliEntradaNumero from "./EliEntradaNumero.vue";
import EliEntradaDataHora from "./EliEntradaDataHora.vue";
export { EliEntradaTexto, EliEntradaNumero, EliEntradaDataHora };
export type { PadroesEntradas, TipoEntrada } from "./tiposEntradas";

View file

@ -0,0 +1,349 @@
export declare const registryTabelaCelulas: {
readonly texto: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
value: {
type: import("vue").PropType<string | null | undefined>;
default: undefined;
};
opcoes: {
type: import("vue").PropType<{
rotulo: string;
placeholder?: string;
} & {
limiteCaracteres?: number;
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep";
}>;
required: true;
};
}>, {
attrs: {
[x: string]: unknown;
};
emit: ((event: "update:value", _v: string | null | undefined) => void) & ((event: "input", _v: string | null | undefined) => void) & ((event: "change", _v: string | null | undefined) => void) & ((event: "focus") => void) & ((event: "blur") => void);
localValue: import("vue").WritableComputedRef<string | null | undefined, string | null | undefined>;
inputHtmlType: import("vue").ComputedRef<"text" | "email" | "url">;
inputMode: import("vue").ComputedRef<string | undefined>;
onInput: (e: Event) => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
"update:value": (_v: string | null | undefined) => true;
input: (_v: string | null | undefined) => true;
change: (_v: string | null | undefined) => true;
focus: () => true;
blur: () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
value: {
type: import("vue").PropType<string | null | undefined>;
default: undefined;
};
opcoes: {
type: import("vue").PropType<{
rotulo: string;
placeholder?: string;
} & {
limiteCaracteres?: number;
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep";
}>;
required: true;
};
}>> & Readonly<{
"onUpdate:value"?: ((_v: string | null | undefined) => any) | undefined;
onInput?: ((_v: string | null | undefined) => any) | undefined;
onChange?: ((_v: string | null | undefined) => any) | undefined;
onFocus?: (() => any) | undefined;
onBlur?: (() => any) | undefined;
}>, {
value: string | null | undefined;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
readonly numero: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
value: {
type: import("vue").PropType<number | null | undefined>;
default: undefined;
};
opcoes: {
type: import("vue").PropType<{
rotulo: string;
placeholder?: string;
} & {
sufixo?: string;
prefixo?: string;
precisao?: number;
}>;
required: true;
};
}>, {
attrs: {
[x: string]: unknown;
};
emit: ((event: "update:value", _v: number | null | undefined) => void) & ((event: "input", _v: number | null | undefined) => void) & ((event: "change", _v: number | null | undefined) => void) & ((event: "focus") => void) & ((event: "blur") => void);
displayValue: import("vue").Ref<string, string>;
isInteiro: import("vue").ComputedRef<boolean>;
onUpdateModelValue: (texto: string) => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
"update:value": (_v: number | null | undefined) => true;
input: (_v: number | null | undefined) => true;
change: (_v: number | null | undefined) => true;
focus: () => true;
blur: () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
value: {
type: import("vue").PropType<number | null | undefined>;
default: undefined;
};
opcoes: {
type: import("vue").PropType<{
rotulo: string;
placeholder?: string;
} & {
sufixo?: string;
prefixo?: string;
precisao?: number;
}>;
required: true;
};
}>> & Readonly<{
"onUpdate:value"?: ((_v: number | null | undefined) => any) | undefined;
onInput?: ((_v: number | null | undefined) => any) | undefined;
onChange?: ((_v: number | null | undefined) => any) | undefined;
onFocus?: (() => any) | undefined;
onBlur?: (() => any) | undefined;
}>, {
value: number | null | undefined;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
readonly dataHora: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
value: {
type: import("vue").PropType<string | null | undefined>;
default: undefined;
};
opcoes: {
type: import("vue").PropType<{
rotulo: string;
placeholder?: string;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../tipos/entrada.js").CampoDensidade;
variante?: import("../../tipos/entrada.js").CampoVariante;
}>;
required: false;
default: undefined;
};
modelValue: {
type: import("vue").PropType<string | null>;
default: null;
};
modo: {
type: import("vue").PropType<"data" | "dataHora" | undefined>;
default: undefined;
};
rotulo: {
type: StringConstructor;
default: undefined;
};
placeholder: {
type: StringConstructor;
default: undefined;
};
desabilitado: {
type: BooleanConstructor;
default: undefined;
};
limpavel: {
type: BooleanConstructor;
default: undefined;
};
erro: {
type: BooleanConstructor;
default: undefined;
};
mensagensErro: {
type: import("vue").PropType<string | string[]>;
default: undefined;
};
dica: {
type: StringConstructor;
default: undefined;
};
dicaPersistente: {
type: BooleanConstructor;
default: undefined;
};
densidade: {
type: import("vue").PropType<import("../../tipos/entrada.js").CampoDensidade>;
default: undefined;
};
variante: {
type: import("vue").PropType<import("../../tipos/entrada.js").CampoVariante>;
default: undefined;
};
min: {
type: import("vue").PropType<string | undefined>;
default: undefined;
};
max: {
type: import("vue").PropType<string | undefined>;
default: undefined;
};
}>, {
attrs: {
[x: string]: unknown;
};
valor: import("vue").WritableComputedRef<string, string>;
tipoInput: import("vue").ComputedRef<"date" | "datetime-local">;
minLocal: import("vue").ComputedRef<string | undefined>;
maxLocal: import("vue").ComputedRef<string | undefined>;
opcoesEfetivas: import("vue").ComputedRef<{
rotulo: string;
placeholder?: string;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../tipos/entrada.js").CampoDensidade;
variante?: import("../../tipos/entrada.js").CampoVariante;
}>;
desabilitadoEfetivo: import("vue").ComputedRef<boolean>;
emitCompatFocus: () => void;
emitCompatBlur: () => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
"update:value": (_v: string | null) => true;
input: (_v: string | null) => true;
change: (_v: string | null) => true;
"update:modelValue": (_v: string | null) => true;
alterar: (_v: string | null) => true;
foco: () => true;
desfoco: () => true;
focus: () => true;
blur: () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
value: {
type: import("vue").PropType<string | null | undefined>;
default: undefined;
};
opcoes: {
type: import("vue").PropType<{
rotulo: string;
placeholder?: string;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../tipos/entrada.js").CampoDensidade;
variante?: import("../../tipos/entrada.js").CampoVariante;
}>;
required: false;
default: undefined;
};
modelValue: {
type: import("vue").PropType<string | null>;
default: null;
};
modo: {
type: import("vue").PropType<"data" | "dataHora" | undefined>;
default: undefined;
};
rotulo: {
type: StringConstructor;
default: undefined;
};
placeholder: {
type: StringConstructor;
default: undefined;
};
desabilitado: {
type: BooleanConstructor;
default: undefined;
};
limpavel: {
type: BooleanConstructor;
default: undefined;
};
erro: {
type: BooleanConstructor;
default: undefined;
};
mensagensErro: {
type: import("vue").PropType<string | string[]>;
default: undefined;
};
dica: {
type: StringConstructor;
default: undefined;
};
dicaPersistente: {
type: BooleanConstructor;
default: undefined;
};
densidade: {
type: import("vue").PropType<import("../../tipos/entrada.js").CampoDensidade>;
default: undefined;
};
variante: {
type: import("vue").PropType<import("../../tipos/entrada.js").CampoVariante>;
default: undefined;
};
min: {
type: import("vue").PropType<string | undefined>;
default: undefined;
};
max: {
type: import("vue").PropType<string | undefined>;
default: undefined;
};
}>> & Readonly<{
"onUpdate:value"?: ((_v: string | null) => any) | undefined;
onInput?: ((_v: string | null) => any) | undefined;
onChange?: ((_v: string | null) => any) | undefined;
onFocus?: (() => any) | undefined;
onBlur?: (() => any) | undefined;
onAlterar?: ((_v: string | null) => any) | undefined;
"onUpdate:modelValue"?: ((_v: string | null) => any) | undefined;
onFoco?: (() => any) | undefined;
onDesfoco?: (() => any) | undefined;
}>, {
modo: "data" | "dataHora" | undefined;
limpavel: boolean;
erro: boolean;
mensagensErro: string | string[];
dica: string;
dicaPersistente: boolean;
min: string | undefined;
max: string | undefined;
densidade: import("../../tipos/entrada.js").CampoDensidade;
variante: import("../../tipos/entrada.js").CampoVariante;
opcoes: {
rotulo: string;
placeholder?: string;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../tipos/entrada.js").CampoDensidade;
variante?: import("../../tipos/entrada.js").CampoVariante;
};
value: string | null | undefined;
placeholder: string;
rotulo: string;
modelValue: string | null;
desabilitado: boolean;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
};

View file

@ -0,0 +1,96 @@
/**
* Tipos base para componentes de entrada (EliEntrada*)
*
* Objetivo:
* - Padronizar o shape de dados de todos os componentes de entrada.
* - Cada entrada possui sempre:
* 1) `value`: o valor atual (tipado)
* 2) `opcoes`: configuração do componente (rótulo, placeholder e extras por tipo)
*
* Como usar:
* - `PadroesEntradas[tipo]` retorna a tipagem completa (value + opcoes) daquele tipo.
* - `TipoEntrada` é a união com todos os tipos suportados (ex.: "texto" | "numero").
*/
/**
* Contrato padrão de uma entrada.
*
* @typeParam T - tipo do `value` (ex.: string | null | undefined)
* @typeParam Mais - campos adicionais dentro de `opcoes`, específicos do tipo de entrada
*/
export type tipoPadraoEntrada<T, Mais extends Record<string, unknown> = {}> = {
/** Valor atual do campo (pode aceitar null/undefined quando aplicável) */
value: T;
/** Configurações do componente (visuais + regras simples do tipo) */
opcoes: {
/** Rótulo exibido ao usuário */
rotulo: string;
/** Texto de ajuda dentro do input quando vazio */
placeholder?: string;
} & Mais;
};
/**
* Mapa de tipos de entrada suportados e suas configurações específicas.
*
* Observação importante:
* - As chaves deste objeto (ex.: "texto", "numero") viram o tipo `TipoEntrada`.
* - Cada item define:
* - `value`: tipo do valor
* - `opcoes`: opções comuns + extras específicas
*/
export type PadroesEntradas = {
texto: tipoPadraoEntrada<string | null | undefined, {
/** Limite máximo de caracteres permitidos (se definido) */
limiteCaracteres?: number;
/**
* Formato/máscara aplicada ao texto.
* Obs: o `value` SEMPRE será o texto formatado (o que aparece no input).
*/
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep";
}>;
numero: tipoPadraoEntrada<number | null | undefined, {
/** Unidade de medida (ex.: "kg", "m³") */
sufixo?: string;
/** Moéda (ex.: "R$") */
prefixo?: string;
/**
* Passo/precisão do valor numérico.
* - 1 => somente inteiros
* - 0.1 => 1 casa decimal
* - 0.01 => 2 casas decimais
*
* Dica: este conceito corresponde ao atributo HTML `step`.
*/
precisao?: number;
}>;
dataHora: tipoPadraoEntrada<string | null | undefined, {
/** Define o tipo de entrada. - `dataHora`: datetime-local - `data`: date */
modo?: "data" | "dataHora";
/** Se true, mostra ícone para limpar o valor (Vuetify clearable). */
limpavel?: boolean;
/** Estado de erro (visual). */
erro?: boolean;
/** Mensagens de erro. */
mensagensErro?: string | string[];
/** Texto de apoio. */
dica?: string;
/** Mantém a dica sempre visível. */
dicaPersistente?: boolean;
/** Valor mínimo permitido (ISO 8601 - offset ou Z). */
min?: string;
/** Valor máximo permitido (ISO 8601 - offset ou Z). */
max?: string;
/** Densidade do campo (Vuetify). */
densidade?: import("../../tipos").CampoDensidade;
/** Variante do v-text-field (Vuetify). */
variante?: import("../../tipos").CampoVariante;
}>;
};
/**
* União dos tipos de entrada suportados.
* Ex.: "texto" | "numero"
*/
export type TipoEntrada = keyof PadroesEntradas;
export type PadraoComponenteEntrada<T extends TipoEntrada> = readonly [T, PadroesEntradas[T]['opcoes']];
export type ComponenteEntrada = {
[K in TipoEntrada]: PadraoComponenteEntrada<K>;
}[TipoEntrada];

View file

@ -0,0 +1,2 @@
/** Formata CEP no padrão 00000-000 */
export declare function formatarCep(valor: string): string;

View file

@ -20,6 +20,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
carregando: import("vue").Ref<boolean, boolean>; carregando: import("vue").Ref<boolean, boolean>;
erro: import("vue").Ref<string | null, string | null>; erro: import("vue").Ref<string | null, string | null>;
linhas: import("vue").Ref<unknown[], unknown[]>; linhas: import("vue").Ref<unknown[], unknown[]>;
linhasPaginadas: import("vue").ComputedRef<unknown[]>;
quantidadeFiltrada: import("vue").ComputedRef<number>;
quantidade: import("vue").Ref<number, number>; quantidade: import("vue").Ref<number, number>;
menuAberto: import("vue").Ref<number | null, number | null>; menuAberto: import("vue").Ref<number | null, number | null>;
valorBusca: import("vue").Ref<string, string>; valorBusca: import("vue").Ref<string, string>;
@ -28,6 +30,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
direcaoOrdenacao: import("vue").Ref<"desc" | "asc", "desc" | "asc">; direcaoOrdenacao: import("vue").Ref<"desc" | "asc", "desc" | "asc">;
totalPaginas: import("vue").ComputedRef<number>; totalPaginas: import("vue").ComputedRef<number>;
exibirBusca: import("vue").ComputedRef<boolean>; exibirBusca: import("vue").ComputedRef<boolean>;
exibirFiltroAvancado: import("vue").ComputedRef<boolean>;
acoesCabecalho: import("vue").ComputedRef<{ acoesCabecalho: import("vue").ComputedRef<{
icone?: import("lucide-vue-next").LucideIcon; icone?: import("lucide-vue-next").LucideIcon;
cor?: string; cor?: string;
@ -50,8 +53,23 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
colunasInvisiveisEfetivas: import("vue").ComputedRef<EliColuna<any>[]>; colunasInvisiveisEfetivas: import("vue").ComputedRef<EliColuna<any>[]>;
linhasExpandidas: import("vue").Ref<Record<number, boolean>, Record<number, boolean>>; linhasExpandidas: import("vue").Ref<Record<number, boolean>, Record<number, boolean>>;
abrirModalColunas: () => void; abrirModalColunas: () => void;
abrirModalFiltro: () => void;
fecharModalColunas: () => void; fecharModalColunas: () => void;
salvarModalColunas: (cfg: EliTabelaColunasConfig) => void; salvarModalColunas: (cfg: EliTabelaColunasConfig) => void;
modalFiltroAberto: import("vue").Ref<boolean, boolean>;
filtrosUi: import("vue").Ref<{
coluna: string | number | symbol;
valor: any;
}[], {
coluna: string | number | symbol;
valor: any;
}[] | {
coluna: string | number | symbol;
valor: any;
}[]>;
salvarFiltrosAvancados: (novo: any[]) => void;
limparFiltrosAvancados: () => void;
fecharModalFiltro: () => void;
alternarLinhaExpandida: (indice: number) => void; alternarLinhaExpandida: (indice: number) => void;
alternarOrdenacao: (chave?: string) => void; alternarOrdenacao: (chave?: string) => void;
atualizarBusca: (texto: string) => void; atualizarBusca: (texto: string) => void;
@ -103,6 +121,11 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
required: false; required: false;
default: boolean; default: boolean;
}; };
exibirBotaoFiltroAvancado: {
type: BooleanConstructor;
required: false;
default: boolean;
};
valorBusca: { valorBusca: {
type: StringConstructor; type: StringConstructor;
required: true; required: true;
@ -132,9 +155,11 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
temAcoesCabecalho: import("vue").ComputedRef<boolean>; temAcoesCabecalho: import("vue").ComputedRef<boolean>;
emitBuscar: (texto: string) => void; emitBuscar: (texto: string) => void;
emitColunas: () => void; emitColunas: () => void;
emitFiltroAvancado: () => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, { }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
buscar(valor: string): boolean; buscar(valor: string): boolean;
colunas(): true; colunas(): true;
filtroAvancado(): true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{ }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
exibirBusca: { exibirBusca: {
type: BooleanConstructor; type: BooleanConstructor;
@ -145,6 +170,11 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
required: false; required: false;
default: boolean; default: boolean;
}; };
exibirBotaoFiltroAvancado: {
type: BooleanConstructor;
required: false;
default: boolean;
};
valorBusca: { valorBusca: {
type: StringConstructor; type: StringConstructor;
required: true; required: true;
@ -173,8 +203,10 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
}>> & Readonly<{ }>> & Readonly<{
onBuscar?: ((valor: string) => any) | undefined; onBuscar?: ((valor: string) => any) | undefined;
onColunas?: (() => any) | undefined; onColunas?: (() => any) | undefined;
onFiltroAvancado?: (() => any) | undefined;
}>, { }>, {
exibirBotaoColunas: boolean; exibirBotaoColunas: boolean;
exibirBotaoFiltroAvancado: boolean;
}, {}, { }, {}, {
EliTabelaCaixaDeBusca: import("vue").DefineComponent<import("vue").ExtractPropTypes<{ EliTabelaCaixaDeBusca: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
modelo: { modelo: {
@ -290,7 +322,6 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
ArrowUp: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>; ArrowUp: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>;
ArrowDown: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>; ArrowDown: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>;
isOrdenavel: (coluna: any) => boolean; isOrdenavel: (coluna: any) => boolean;
obterClasseAlinhamento: (alinhamento?: string) => "eli-tabela__celula--direita" | "eli-tabela__celula--centro" | "eli-tabela__celula--esquerda";
emitAlternarOrdenacao: (chave: string) => void; emitAlternarOrdenacao: (chave: string) => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, { }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
alternarOrdenacao(chave: string): boolean; alternarOrdenacao(chave: string): boolean;
@ -365,9 +396,6 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
}>, { }>, {
ChevronRight: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>; ChevronRight: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>;
ChevronDown: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>; ChevronDown: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>;
obterClasseAlinhamento: (alinhamento?: string) => "eli-tabela__celula--direita" | "eli-tabela__celula--centro" | "eli-tabela__celula--esquerda";
obterMaxWidth: (largura?: number | string) => string | undefined;
obterTooltipCelula: (celula: unknown) => any;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
colunas: { colunas: {
type: PropType<Array<EliColuna<any>>>; type: PropType<Array<EliColuna<any>>>;
@ -420,6 +448,9 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
dadosParaComponente: import("vue").ComputedRef<{ dadosParaComponente: import("vue").ComputedRef<{
texto: string; texto: string;
acao?: () => void; acao?: () => void;
} | {
texto: string;
acao?: () => void;
} | { } | {
numero: number; numero: number;
acao?: () => void; acao?: () => void;
@ -459,6 +490,9 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
dadosParaComponente: import("vue").ComputedRef<{ dadosParaComponente: import("vue").ComputedRef<{
texto: string; texto: string;
acao?: () => void; acao?: () => void;
} | {
texto: string;
acao?: () => void;
} | { } | {
numero: number; numero: number;
acao?: () => void; acao?: () => void;
@ -595,6 +629,10 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
type: PropType<EliTabelaColunasConfig>; type: PropType<EliTabelaColunasConfig>;
required: true; required: true;
}; };
colunas: {
type: PropType<Array<EliColuna<any>>>;
required: true;
};
}>, { }>, {
visiveisLocal: import("vue").Ref<string[], string[]>; visiveisLocal: import("vue").Ref<string[], string[]>;
invisiveisLocal: import("vue").Ref<string[], string[]>; invisiveisLocal: import("vue").Ref<string[], string[]>;
@ -619,10 +657,488 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
type: PropType<EliTabelaColunasConfig>; type: PropType<EliTabelaColunasConfig>;
required: true; required: true;
}; };
colunas: {
type: PropType<Array<EliColuna<any>>>;
required: true;
};
}>> & Readonly<{ }>> & Readonly<{
onFechar?: (() => any) | undefined; onFechar?: (() => any) | undefined;
onSalvar?: ((_config: EliTabelaColunasConfig) => any) | undefined; onSalvar?: ((_config: EliTabelaColunasConfig) => any) | undefined;
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>; }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
EliTabelaModalFiltroAvancado: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
aberto: {
type: BooleanConstructor;
required: true;
};
filtrosBase: {
type: PropType<Array<{
rotulo: string;
coluna: string | number | symbol;
operador: import("p-comuns").operadores | keyof typeof import("p-comuns").operadores;
entrada: import("../EliEntrada/tiposEntradas.js").ComponenteEntrada;
}>>;
required: true;
};
modelo: {
type: PropType<Array<any>>;
required: true;
};
}>, {
linhas: import("vue").Ref<{
coluna: string | number | symbol;
entrada: readonly ["texto", {
rotulo: string;
placeholder?: string | undefined;
limiteCaracteres?: number | undefined;
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep" | undefined;
}] | readonly ["dataHora", {
rotulo: string;
placeholder?: string | undefined;
modo?: "data" | "dataHora" | undefined;
limpavel?: boolean | undefined;
erro?: boolean | undefined;
mensagensErro?: string | string[] | undefined;
dica?: string | undefined;
dicaPersistente?: boolean | undefined;
min?: string | undefined;
max?: string | undefined;
densidade?: import("../../tipos/entrada.js").CampoDensidade | undefined;
variante?: import("../../tipos/entrada.js").CampoVariante | undefined;
}] | readonly ["numero", {
rotulo: string;
placeholder?: string | undefined;
sufixo?: string | undefined;
prefixo?: string | undefined;
precisao?: number | undefined;
}];
operador: string;
valor: any;
}[], {
coluna: string | number | symbol;
entrada: import("../EliEntrada/tiposEntradas.js").ComponenteEntrada;
operador: string;
valor: any;
}[] | {
coluna: string | number | symbol;
entrada: readonly ["texto", {
rotulo: string;
placeholder?: string | undefined;
limiteCaracteres?: number | undefined;
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep" | undefined;
}] | readonly ["dataHora", {
rotulo: string;
placeholder?: string | undefined;
modo?: "data" | "dataHora" | undefined;
limpavel?: boolean | undefined;
erro?: boolean | undefined;
mensagensErro?: string | string[] | undefined;
dica?: string | undefined;
dicaPersistente?: boolean | undefined;
min?: string | undefined;
max?: string | undefined;
densidade?: import("../../tipos/entrada.js").CampoDensidade | undefined;
variante?: import("../../tipos/entrada.js").CampoVariante | undefined;
}] | readonly ["numero", {
rotulo: string;
placeholder?: string | undefined;
sufixo?: string | undefined;
prefixo?: string | undefined;
precisao?: number | undefined;
}];
operador: string;
valor: any;
}[]>;
opcoesParaAdicionar: import("vue").ComputedRef<{
rotulo: string;
coluna: string | number | symbol;
operador: import("p-comuns").operadores | keyof typeof import("p-comuns").operadores;
entrada: import("../EliEntrada/tiposEntradas.js").ComponenteEntrada;
}[]>;
colunaParaAdicionar: import("vue").Ref<string, string>;
componenteEntrada: (entrada: import("../EliEntrada/tiposEntradas.js").ComponenteEntrada) => import("vue").DefineComponent<import("vue").ExtractPropTypes<{
value: {
type: PropType<string | null | undefined>;
default: undefined;
};
opcoes: {
type: PropType<{
rotulo: string;
placeholder?: string;
} & {
limiteCaracteres?: number;
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep";
}>;
required: true;
};
}>, {
attrs: {
[x: string]: unknown;
};
emit: ((event: "update:value", _v: string | null | undefined) => void) & ((event: "input", _v: string | null | undefined) => void) & ((event: "change", _v: string | null | undefined) => void) & ((event: "focus") => void) & ((event: "blur") => void);
localValue: import("vue").WritableComputedRef<string | null | undefined, string | null | undefined>;
inputHtmlType: import("vue").ComputedRef<"text" | "email" | "url">;
inputMode: import("vue").ComputedRef<string | undefined>;
onInput: (e: Event) => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
"update:value": (_v: string | null | undefined) => true;
input: (_v: string | null | undefined) => true;
change: (_v: string | null | undefined) => true;
focus: () => true;
blur: () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
value: {
type: PropType<string | null | undefined>;
default: undefined;
};
opcoes: {
type: PropType<{
rotulo: string;
placeholder?: string;
} & {
limiteCaracteres?: number;
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep";
}>;
required: true;
};
}>> & Readonly<{
"onUpdate:value"?: ((_v: string | null | undefined) => any) | undefined;
onInput?: ((_v: string | null | undefined) => any) | undefined;
onChange?: ((_v: string | null | undefined) => any) | undefined;
onFocus?: (() => any) | undefined;
onBlur?: (() => any) | undefined;
}>, {
value: string | null | undefined;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any> | import("vue").DefineComponent<import("vue").ExtractPropTypes<{
value: {
type: PropType<number | null | undefined>;
default: undefined;
};
opcoes: {
type: PropType<{
rotulo: string;
placeholder?: string;
} & {
sufixo?: string;
prefixo?: string;
precisao?: number;
}>;
required: true;
};
}>, {
attrs: {
[x: string]: unknown;
};
emit: ((event: "update:value", _v: number | null | undefined) => void) & ((event: "input", _v: number | null | undefined) => void) & ((event: "change", _v: number | null | undefined) => void) & ((event: "focus") => void) & ((event: "blur") => void);
displayValue: import("vue").Ref<string, string>;
isInteiro: import("vue").ComputedRef<boolean>;
onUpdateModelValue: (texto: string) => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
"update:value": (_v: number | null | undefined) => true;
input: (_v: number | null | undefined) => true;
change: (_v: number | null | undefined) => true;
focus: () => true;
blur: () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
value: {
type: PropType<number | null | undefined>;
default: undefined;
};
opcoes: {
type: PropType<{
rotulo: string;
placeholder?: string;
} & {
sufixo?: string;
prefixo?: string;
precisao?: number;
}>;
required: true;
};
}>> & Readonly<{
"onUpdate:value"?: ((_v: number | null | undefined) => any) | undefined;
onInput?: ((_v: number | null | undefined) => any) | undefined;
onChange?: ((_v: number | null | undefined) => any) | undefined;
onFocus?: (() => any) | undefined;
onBlur?: (() => any) | undefined;
}>, {
value: number | null | undefined;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any> | import("vue").DefineComponent<import("vue").ExtractPropTypes<{
value: {
type: PropType<string | null | undefined>;
default: undefined;
};
opcoes: {
type: PropType<{
rotulo: string;
placeholder?: string;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../tipos/entrada.js").CampoDensidade;
variante?: import("../../tipos/entrada.js").CampoVariante;
}>;
required: false;
default: undefined;
};
modelValue: {
type: PropType<string | null>;
default: null;
};
modo: {
type: PropType<"data" | "dataHora" | undefined>;
default: undefined;
};
rotulo: {
type: StringConstructor;
default: undefined;
};
placeholder: {
type: StringConstructor;
default: undefined;
};
desabilitado: {
type: BooleanConstructor;
default: undefined;
};
limpavel: {
type: BooleanConstructor;
default: undefined;
};
erro: {
type: BooleanConstructor;
default: undefined;
};
mensagensErro: {
type: PropType<string | string[]>;
default: undefined;
};
dica: {
type: StringConstructor;
default: undefined;
};
dicaPersistente: {
type: BooleanConstructor;
default: undefined;
};
densidade: {
type: PropType<import("../../tipos/entrada.js").CampoDensidade>;
default: undefined;
};
variante: {
type: PropType<import("../../tipos/entrada.js").CampoVariante>;
default: undefined;
};
min: {
type: PropType<string | undefined>;
default: undefined;
};
max: {
type: PropType<string | undefined>;
default: undefined;
};
}>, {
attrs: {
[x: string]: unknown;
};
valor: import("vue").WritableComputedRef<string, string>;
tipoInput: import("vue").ComputedRef<"date" | "datetime-local">;
minLocal: import("vue").ComputedRef<string | undefined>;
maxLocal: import("vue").ComputedRef<string | undefined>;
opcoesEfetivas: import("vue").ComputedRef<{
rotulo: string;
placeholder?: string;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../tipos/entrada.js").CampoDensidade;
variante?: import("../../tipos/entrada.js").CampoVariante;
}>;
desabilitadoEfetivo: import("vue").ComputedRef<boolean>;
emitCompatFocus: () => void;
emitCompatBlur: () => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
"update:value": (_v: string | null) => true;
input: (_v: string | null) => true;
change: (_v: string | null) => true;
"update:modelValue": (_v: string | null) => true;
alterar: (_v: string | null) => true;
foco: () => true;
desfoco: () => true;
focus: () => true;
blur: () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
value: {
type: PropType<string | null | undefined>;
default: undefined;
};
opcoes: {
type: PropType<{
rotulo: string;
placeholder?: string;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../tipos/entrada.js").CampoDensidade;
variante?: import("../../tipos/entrada.js").CampoVariante;
}>;
required: false;
default: undefined;
};
modelValue: {
type: PropType<string | null>;
default: null;
};
modo: {
type: PropType<"data" | "dataHora" | undefined>;
default: undefined;
};
rotulo: {
type: StringConstructor;
default: undefined;
};
placeholder: {
type: StringConstructor;
default: undefined;
};
desabilitado: {
type: BooleanConstructor;
default: undefined;
};
limpavel: {
type: BooleanConstructor;
default: undefined;
};
erro: {
type: BooleanConstructor;
default: undefined;
};
mensagensErro: {
type: PropType<string | string[]>;
default: undefined;
};
dica: {
type: StringConstructor;
default: undefined;
};
dicaPersistente: {
type: BooleanConstructor;
default: undefined;
};
densidade: {
type: PropType<import("../../tipos/entrada.js").CampoDensidade>;
default: undefined;
};
variante: {
type: PropType<import("../../tipos/entrada.js").CampoVariante>;
default: undefined;
};
min: {
type: PropType<string | undefined>;
default: undefined;
};
max: {
type: PropType<string | undefined>;
default: undefined;
};
}>> & Readonly<{
"onUpdate:value"?: ((_v: string | null) => any) | undefined;
onInput?: ((_v: string | null) => any) | undefined;
onChange?: ((_v: string | null) => any) | undefined;
onFocus?: (() => any) | undefined;
onBlur?: (() => any) | undefined;
onAlterar?: ((_v: string | null) => any) | undefined;
"onUpdate:modelValue"?: ((_v: string | null) => any) | undefined;
onFoco?: (() => any) | undefined;
onDesfoco?: (() => any) | undefined;
}>, {
modo: "data" | "dataHora" | undefined;
limpavel: boolean;
erro: boolean;
mensagensErro: string | string[];
dica: string;
dicaPersistente: boolean;
min: string | undefined;
max: string | undefined;
densidade: import("../../tipos/entrada.js").CampoDensidade;
variante: import("../../tipos/entrada.js").CampoVariante;
opcoes: {
rotulo: string;
placeholder?: string;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../tipos/entrada.js").CampoDensidade;
variante?: import("../../tipos/entrada.js").CampoVariante;
};
value: string | null | undefined;
placeholder: string;
rotulo: string;
modelValue: string | null;
desabilitado: boolean;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
opcoesEntrada: (entrada: import("../EliEntrada/tiposEntradas.js").ComponenteEntrada) => any;
adicionar: () => void;
remover: (idx: number) => void;
emitFechar: () => void;
emitSalvar: () => void;
emitLimpar: () => void;
rotuloDoFiltro: (f: {
rotulo: string;
coluna: string | number | symbol;
operador: import("p-comuns").operadores | keyof typeof import("p-comuns").operadores;
entrada: import("../EliEntrada/tiposEntradas.js").ComponenteEntrada;
}) => string;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
fechar: () => true;
limpar: () => true;
salvar: (_linhas: any[]) => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
aberto: {
type: BooleanConstructor;
required: true;
};
filtrosBase: {
type: PropType<Array<{
rotulo: string;
coluna: string | number | symbol;
operador: import("p-comuns").operadores | keyof typeof import("p-comuns").operadores;
entrada: import("../EliEntrada/tiposEntradas.js").ComponenteEntrada;
}>>;
required: true;
};
modelo: {
type: PropType<Array<any>>;
required: true;
};
}>> & Readonly<{
onFechar?: (() => any) | undefined;
onSalvar?: ((_linhas: any[]) => any) | undefined;
onLimpar?: (() => any) | undefined;
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>; }, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
declare const _default: typeof __VLS_export; declare const _default: typeof __VLS_export;
export default _default; export default _default;

View file

@ -44,9 +44,6 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
}>, { }>, {
ChevronRight: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>; ChevronRight: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>;
ChevronDown: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>; ChevronDown: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>;
obterClasseAlinhamento: (alinhamento?: string) => "eli-tabela__celula--direita" | "eli-tabela__celula--centro" | "eli-tabela__celula--esquerda";
obterMaxWidth: (largura?: number | string) => string | undefined;
obterTooltipCelula: (celula: unknown) => any;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
colunas: { colunas: {
type: PropType<Array<EliColuna<any>>>; type: PropType<Array<EliColuna<any>>>;
@ -99,6 +96,9 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
dadosParaComponente: import("vue").ComputedRef<{ dadosParaComponente: import("vue").ComputedRef<{
texto: string; texto: string;
acao?: () => void; acao?: () => void;
} | {
texto: string;
acao?: () => void;
} | { } | {
numero: number; numero: number;
acao?: () => void; acao?: () => void;
@ -138,6 +138,9 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
dadosParaComponente: import("vue").ComputedRef<{ dadosParaComponente: import("vue").ComputedRef<{
texto: string; texto: string;
acao?: () => void; acao?: () => void;
} | {
texto: string;
acao?: () => void;
} | { } | {
numero: number; numero: number;
acao?: () => void; acao?: () => void;

View file

@ -9,6 +9,11 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
required: false; required: false;
default: boolean; default: boolean;
}; };
exibirBotaoFiltroAvancado: {
type: BooleanConstructor;
required: false;
default: boolean;
};
valorBusca: { valorBusca: {
type: StringConstructor; type: StringConstructor;
required: true; required: true;
@ -26,9 +31,11 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
temAcoesCabecalho: import("vue").ComputedRef<boolean>; temAcoesCabecalho: import("vue").ComputedRef<boolean>;
emitBuscar: (texto: string) => void; emitBuscar: (texto: string) => void;
emitColunas: () => void; emitColunas: () => void;
emitFiltroAvancado: () => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, { }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
buscar(valor: string): boolean; buscar(valor: string): boolean;
colunas(): true; colunas(): true;
filtroAvancado(): true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{ }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
exibirBusca: { exibirBusca: {
type: BooleanConstructor; type: BooleanConstructor;
@ -39,6 +46,11 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
required: false; required: false;
default: boolean; default: boolean;
}; };
exibirBotaoFiltroAvancado: {
type: BooleanConstructor;
required: false;
default: boolean;
};
valorBusca: { valorBusca: {
type: StringConstructor; type: StringConstructor;
required: true; required: true;
@ -55,8 +67,10 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
}>> & Readonly<{ }>> & Readonly<{
onBuscar?: ((valor: string) => any) | undefined; onBuscar?: ((valor: string) => any) | undefined;
onColunas?: (() => any) | undefined; onColunas?: (() => any) | undefined;
onFiltroAvancado?: (() => any) | undefined;
}>, { }>, {
exibirBotaoColunas: boolean; exibirBotaoColunas: boolean;
exibirBotaoFiltroAvancado: boolean;
}, {}, { }, {}, {
EliTabelaCaixaDeBusca: import("vue").DefineComponent<import("vue").ExtractPropTypes<{ EliTabelaCaixaDeBusca: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
modelo: { modelo: {

View file

@ -29,6 +29,9 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
dadosParaComponente: import("vue").ComputedRef<{ dadosParaComponente: import("vue").ComputedRef<{
texto: string; texto: string;
acao?: () => void; acao?: () => void;
} | {
texto: string;
acao?: () => void;
} | { } | {
numero: number; numero: number;
acao?: () => void; acao?: () => void;

View file

@ -25,7 +25,6 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
ArrowUp: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>; ArrowUp: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>;
ArrowDown: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>; ArrowDown: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>;
isOrdenavel: (coluna: any) => boolean; isOrdenavel: (coluna: any) => boolean;
obterClasseAlinhamento: (alinhamento?: string) => "eli-tabela__celula--direita" | "eli-tabela__celula--centro" | "eli-tabela__celula--esquerda";
emitAlternarOrdenacao: (chave: string) => void; emitAlternarOrdenacao: (chave: string) => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, { }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
alternarOrdenacao(chave: string): boolean; alternarOrdenacao(chave: string): boolean;

View file

@ -1,5 +1,6 @@
import { PropType } from "vue"; import { PropType } from "vue";
import type { EliTabelaColunasConfig } from "./colunasStorage"; import type { EliTabelaColunasConfig } from "./colunasStorage";
import type { EliColuna } from "./types-eli-tabela";
type OrigemLista = "visiveis" | "invisiveis"; type OrigemLista = "visiveis" | "invisiveis";
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
aberto: { aberto: {
@ -14,6 +15,10 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
type: PropType<EliTabelaColunasConfig>; type: PropType<EliTabelaColunasConfig>;
required: true; required: true;
}; };
colunas: {
type: PropType<Array<EliColuna<any>>>;
required: true;
};
}>, { }>, {
visiveisLocal: import("vue").Ref<string[], string[]>; visiveisLocal: import("vue").Ref<string[], string[]>;
invisiveisLocal: import("vue").Ref<string[], string[]>; invisiveisLocal: import("vue").Ref<string[], string[]>;
@ -38,6 +43,10 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
type: PropType<EliTabelaColunasConfig>; type: PropType<EliTabelaColunasConfig>;
required: true; required: true;
}; };
colunas: {
type: PropType<Array<EliColuna<any>>>;
required: true;
};
}>> & Readonly<{ }>> & Readonly<{
onFechar?: (() => any) | undefined; onFechar?: (() => any) | undefined;
onSalvar?: ((_config: EliTabelaColunasConfig) => any) | undefined; onSalvar?: ((_config: EliTabelaColunasConfig) => any) | undefined;

View file

@ -0,0 +1,467 @@
import { PropType } from "vue";
import type { ComponenteEntrada } from "../EliEntrada/tiposEntradas";
import type { EliTabelaConsulta } from "./types-eli-tabela";
type FiltroBase<T> = NonNullable<EliTabelaConsulta<T>["filtroAvancado"]>[number];
type LinhaFiltro<T> = {
coluna: keyof T;
entrada: ComponenteEntrada;
operador: string;
valor: any;
};
declare function rotuloDoFiltro(f: FiltroBase<any>): string;
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
aberto: {
type: BooleanConstructor;
required: true;
};
filtrosBase: {
type: PropType<Array<FiltroBase<any>>>;
required: true;
};
modelo: {
type: PropType<Array<any>>;
required: true;
};
}>, {
linhas: import("vue").Ref<{
coluna: string | number | symbol;
entrada: readonly ["texto", {
rotulo: string;
placeholder?: string | undefined;
limiteCaracteres?: number | undefined;
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep" | undefined;
}] | readonly ["dataHora", {
rotulo: string;
placeholder?: string | undefined;
modo?: "data" | "dataHora" | undefined;
limpavel?: boolean | undefined;
erro?: boolean | undefined;
mensagensErro?: string | string[] | undefined;
dica?: string | undefined;
dicaPersistente?: boolean | undefined;
min?: string | undefined;
max?: string | undefined;
densidade?: import("../../tipos").CampoDensidade | undefined;
variante?: import("../../tipos").CampoVariante | undefined;
}] | readonly ["numero", {
rotulo: string;
placeholder?: string | undefined;
sufixo?: string | undefined;
prefixo?: string | undefined;
precisao?: number | undefined;
}];
operador: string;
valor: any;
}[], LinhaFiltro<any>[] | {
coluna: string | number | symbol;
entrada: readonly ["texto", {
rotulo: string;
placeholder?: string | undefined;
limiteCaracteres?: number | undefined;
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep" | undefined;
}] | readonly ["dataHora", {
rotulo: string;
placeholder?: string | undefined;
modo?: "data" | "dataHora" | undefined;
limpavel?: boolean | undefined;
erro?: boolean | undefined;
mensagensErro?: string | string[] | undefined;
dica?: string | undefined;
dicaPersistente?: boolean | undefined;
min?: string | undefined;
max?: string | undefined;
densidade?: import("../../tipos").CampoDensidade | undefined;
variante?: import("../../tipos").CampoVariante | undefined;
}] | readonly ["numero", {
rotulo: string;
placeholder?: string | undefined;
sufixo?: string | undefined;
prefixo?: string | undefined;
precisao?: number | undefined;
}];
operador: string;
valor: any;
}[]>;
opcoesParaAdicionar: import("vue").ComputedRef<{
rotulo: string;
coluna: string | number | symbol;
operador: import("p-comuns").operadores | keyof typeof import("p-comuns").operadores;
entrada: ComponenteEntrada;
}[]>;
colunaParaAdicionar: import("vue").Ref<string, string>;
componenteEntrada: (entrada: ComponenteEntrada) => import("vue").DefineComponent<import("vue").ExtractPropTypes<{
value: {
type: PropType<string | null | undefined>;
default: undefined;
};
opcoes: {
type: PropType<{
rotulo: string;
placeholder?: string;
} & {
limiteCaracteres?: number;
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep";
}>;
required: true;
};
}>, {
attrs: {
[x: string]: unknown;
};
emit: ((event: "update:value", _v: string | null | undefined) => void) & ((event: "input", _v: string | null | undefined) => void) & ((event: "change", _v: string | null | undefined) => void) & ((event: "focus") => void) & ((event: "blur") => void);
localValue: import("vue").WritableComputedRef<string | null | undefined, string | null | undefined>;
inputHtmlType: import("vue").ComputedRef<"text" | "email" | "url">;
inputMode: import("vue").ComputedRef<string | undefined>;
onInput: (e: Event) => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
"update:value": (_v: string | null | undefined) => true;
input: (_v: string | null | undefined) => true;
change: (_v: string | null | undefined) => true;
focus: () => true;
blur: () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
value: {
type: PropType<string | null | undefined>;
default: undefined;
};
opcoes: {
type: PropType<{
rotulo: string;
placeholder?: string;
} & {
limiteCaracteres?: number;
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep";
}>;
required: true;
};
}>> & Readonly<{
"onUpdate:value"?: ((_v: string | null | undefined) => any) | undefined;
onInput?: ((_v: string | null | undefined) => any) | undefined;
onChange?: ((_v: string | null | undefined) => any) | undefined;
onFocus?: (() => any) | undefined;
onBlur?: (() => any) | undefined;
}>, {
value: string | null | undefined;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any> | import("vue").DefineComponent<import("vue").ExtractPropTypes<{
value: {
type: PropType<number | null | undefined>;
default: undefined;
};
opcoes: {
type: PropType<{
rotulo: string;
placeholder?: string;
} & {
sufixo?: string;
prefixo?: string;
precisao?: number;
}>;
required: true;
};
}>, {
attrs: {
[x: string]: unknown;
};
emit: ((event: "update:value", _v: number | null | undefined) => void) & ((event: "input", _v: number | null | undefined) => void) & ((event: "change", _v: number | null | undefined) => void) & ((event: "focus") => void) & ((event: "blur") => void);
displayValue: import("vue").Ref<string, string>;
isInteiro: import("vue").ComputedRef<boolean>;
onUpdateModelValue: (texto: string) => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
"update:value": (_v: number | null | undefined) => true;
input: (_v: number | null | undefined) => true;
change: (_v: number | null | undefined) => true;
focus: () => true;
blur: () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
value: {
type: PropType<number | null | undefined>;
default: undefined;
};
opcoes: {
type: PropType<{
rotulo: string;
placeholder?: string;
} & {
sufixo?: string;
prefixo?: string;
precisao?: number;
}>;
required: true;
};
}>> & Readonly<{
"onUpdate:value"?: ((_v: number | null | undefined) => any) | undefined;
onInput?: ((_v: number | null | undefined) => any) | undefined;
onChange?: ((_v: number | null | undefined) => any) | undefined;
onFocus?: (() => any) | undefined;
onBlur?: (() => any) | undefined;
}>, {
value: number | null | undefined;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any> | import("vue").DefineComponent<import("vue").ExtractPropTypes<{
value: {
type: PropType<string | null | undefined>;
default: undefined;
};
opcoes: {
type: PropType<{
rotulo: string;
placeholder?: string;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../tipos").CampoDensidade;
variante?: import("../../tipos").CampoVariante;
}>;
required: false;
default: undefined;
};
modelValue: {
type: PropType<string | null>;
default: null;
};
modo: {
type: PropType<"data" | "dataHora" | undefined>;
default: undefined;
};
rotulo: {
type: StringConstructor;
default: undefined;
};
placeholder: {
type: StringConstructor;
default: undefined;
};
desabilitado: {
type: BooleanConstructor;
default: undefined;
};
limpavel: {
type: BooleanConstructor;
default: undefined;
};
erro: {
type: BooleanConstructor;
default: undefined;
};
mensagensErro: {
type: PropType<string | string[]>;
default: undefined;
};
dica: {
type: StringConstructor;
default: undefined;
};
dicaPersistente: {
type: BooleanConstructor;
default: undefined;
};
densidade: {
type: PropType<import("../../tipos").CampoDensidade>;
default: undefined;
};
variante: {
type: PropType<import("../../tipos").CampoVariante>;
default: undefined;
};
min: {
type: PropType<string | undefined>;
default: undefined;
};
max: {
type: PropType<string | undefined>;
default: undefined;
};
}>, {
attrs: {
[x: string]: unknown;
};
valor: import("vue").WritableComputedRef<string, string>;
tipoInput: import("vue").ComputedRef<"date" | "datetime-local">;
minLocal: import("vue").ComputedRef<string | undefined>;
maxLocal: import("vue").ComputedRef<string | undefined>;
opcoesEfetivas: import("vue").ComputedRef<{
rotulo: string;
placeholder?: string;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../tipos").CampoDensidade;
variante?: import("../../tipos").CampoVariante;
}>;
desabilitadoEfetivo: import("vue").ComputedRef<boolean>;
emitCompatFocus: () => void;
emitCompatBlur: () => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
"update:value": (_v: string | null) => true;
input: (_v: string | null) => true;
change: (_v: string | null) => true;
"update:modelValue": (_v: string | null) => true;
alterar: (_v: string | null) => true;
foco: () => true;
desfoco: () => true;
focus: () => true;
blur: () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
value: {
type: PropType<string | null | undefined>;
default: undefined;
};
opcoes: {
type: PropType<{
rotulo: string;
placeholder?: string;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../tipos").CampoDensidade;
variante?: import("../../tipos").CampoVariante;
}>;
required: false;
default: undefined;
};
modelValue: {
type: PropType<string | null>;
default: null;
};
modo: {
type: PropType<"data" | "dataHora" | undefined>;
default: undefined;
};
rotulo: {
type: StringConstructor;
default: undefined;
};
placeholder: {
type: StringConstructor;
default: undefined;
};
desabilitado: {
type: BooleanConstructor;
default: undefined;
};
limpavel: {
type: BooleanConstructor;
default: undefined;
};
erro: {
type: BooleanConstructor;
default: undefined;
};
mensagensErro: {
type: PropType<string | string[]>;
default: undefined;
};
dica: {
type: StringConstructor;
default: undefined;
};
dicaPersistente: {
type: BooleanConstructor;
default: undefined;
};
densidade: {
type: PropType<import("../../tipos").CampoDensidade>;
default: undefined;
};
variante: {
type: PropType<import("../../tipos").CampoVariante>;
default: undefined;
};
min: {
type: PropType<string | undefined>;
default: undefined;
};
max: {
type: PropType<string | undefined>;
default: undefined;
};
}>> & Readonly<{
"onUpdate:value"?: ((_v: string | null) => any) | undefined;
onInput?: ((_v: string | null) => any) | undefined;
onChange?: ((_v: string | null) => any) | undefined;
onFocus?: (() => any) | undefined;
onBlur?: (() => any) | undefined;
onAlterar?: ((_v: string | null) => any) | undefined;
"onUpdate:modelValue"?: ((_v: string | null) => any) | undefined;
onFoco?: (() => any) | undefined;
onDesfoco?: (() => any) | undefined;
}>, {
modo: "data" | "dataHora" | undefined;
limpavel: boolean;
erro: boolean;
mensagensErro: string | string[];
dica: string;
dicaPersistente: boolean;
min: string | undefined;
max: string | undefined;
densidade: import("../../tipos").CampoDensidade;
variante: import("../../tipos").CampoVariante;
opcoes: {
rotulo: string;
placeholder?: string;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../tipos").CampoDensidade;
variante?: import("../../tipos").CampoVariante;
};
value: string | null | undefined;
placeholder: string;
rotulo: string;
modelValue: string | null;
desabilitado: boolean;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
opcoesEntrada: (entrada: ComponenteEntrada) => any;
adicionar: () => void;
remover: (idx: number) => void;
emitFechar: () => void;
emitSalvar: () => void;
emitLimpar: () => void;
rotuloDoFiltro: typeof rotuloDoFiltro;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
fechar: () => true;
limpar: () => true;
salvar: (_linhas: any[]) => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
aberto: {
type: BooleanConstructor;
required: true;
};
filtrosBase: {
type: PropType<Array<FiltroBase<any>>>;
required: true;
};
modelo: {
type: PropType<Array<any>>;
required: true;
};
}>> & Readonly<{
onFechar?: (() => any) | undefined;
onSalvar?: ((_linhas: any[]) => any) | undefined;
onLimpar?: (() => any) | undefined;
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
declare const _default: typeof __VLS_export;
export default _default;

View file

@ -11,6 +11,9 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
dadosParaComponente: import("vue").ComputedRef<{ dadosParaComponente: import("vue").ComputedRef<{
texto: string; texto: string;
acao?: () => void; acao?: () => void;
} | {
texto: string;
acao?: () => void;
} | { } | {
numero: number; numero: number;
acao?: () => void; acao?: () => void;

View file

@ -0,0 +1,18 @@
import { PropType } from "vue";
import type { TiposTabelaCelulas } from "./tiposTabelaCelulas";
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
dados: {
type: PropType<TiposTabelaCelulas["textoTruncado"]>;
};
}>, {
dados: {
texto: string;
acao?: () => void;
} | undefined;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
dados: {
type: PropType<TiposTabelaCelulas["textoTruncado"]>;
};
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
declare const _default: typeof __VLS_export;
export default _default;

View file

@ -13,6 +13,20 @@ export declare const registryTabelaCelulas: {
type: import("vue").PropType<import("./tiposTabelaCelulas").TiposTabelaCelulas["textoSimples"]>; type: import("vue").PropType<import("./tiposTabelaCelulas").TiposTabelaCelulas["textoSimples"]>;
}; };
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>; }>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
readonly textoTruncado: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
dados: {
type: import("vue").PropType<import("./tiposTabelaCelulas").TiposTabelaCelulas["textoTruncado"]>;
};
}>, {
dados: {
texto: string;
acao?: () => void;
} | undefined;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
dados: {
type: import("vue").PropType<import("./tiposTabelaCelulas").TiposTabelaCelulas["textoTruncado"]>;
};
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
readonly numero: import("vue").DefineComponent<import("vue").ExtractPropTypes<{ readonly numero: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
dados: { dados: {
type: import("vue").PropType<import("./tiposTabelaCelulas").TiposTabelaCelulas["numero"]>; type: import("vue").PropType<import("./tiposTabelaCelulas").TiposTabelaCelulas["numero"]>;

View file

@ -6,6 +6,10 @@ export type TiposTabelaCelulas = {
texto: string; texto: string;
acao?: () => void; acao?: () => void;
}; };
textoTruncado: {
texto: string;
acao?: () => void;
};
numero: { numero: {
numero: number; numero: number;
acao?: () => void; acao?: () => void;

View file

@ -0,0 +1,7 @@
export type EliTabelaFiltroAvancadoSalvo<T> = Array<{
coluna: keyof T;
valor: any;
}>;
export declare function carregarFiltroAvancado<T>(nomeTabela: string): EliTabelaFiltroAvancadoSalvo<T>;
export declare function salvarFiltroAvancado<T>(nomeTabela: string, filtros: EliTabelaFiltroAvancadoSalvo<T>): void;
export declare function limparFiltroAvancado(nomeTabela: string): void;

View file

@ -1,38 +1,31 @@
import type { tipoResposta } from "p-respostas"; import type { tipoResposta } from "p-respostas";
import type { LucideIcon } from "lucide-vue-next"; import type { LucideIcon } from "lucide-vue-next";
import type { TipoTabelaCelula, TiposTabelaCelulas } from "./celulas/tiposTabelaCelulas"; import type { TipoTabelaCelula, TiposTabelaCelulas } from "./celulas/tiposTabelaCelulas";
import { operadores, zFiltro } from "p-comuns";
import { ComponenteEntrada } from "../EliEntrada/tiposEntradas";
export type tipoFiltro = ReturnType<(typeof zFiltro)["parse"]>;
export type ComponenteCelulaBase<T extends TipoTabelaCelula> = readonly [T, TiposTabelaCelulas[T]]; export type ComponenteCelulaBase<T extends TipoTabelaCelula> = readonly [T, TiposTabelaCelulas[T]];
export type ComponenteCelula = { export type ComponenteCelula = {
[K in TipoTabelaCelula]: ComponenteCelulaBase<K>; [K in TipoTabelaCelula]: ComponenteCelulaBase<K>;
}[TipoTabelaCelula]; }[TipoTabelaCelula];
export declare const celulaTabela: <T extends TipoTabelaCelula>(tipo: T, dados: TiposTabelaCelulas[T]) => ComponenteCelulaBase<T>; export declare const celulaTabela: <T extends TipoTabelaCelula>(tipo: T, dados: TiposTabelaCelulas[T]) => ComponenteCelulaBase<T>;
export type { TipoTabelaCelula, TiposTabelaCelulas }; export type { TipoTabelaCelula, TiposTabelaCelulas };
export type EliAlinhamentoColuna = "esquerda" | "centro" | "direita";
export type EliColuna<T> = { export type EliColuna<T> = {
/** Texto exibido no cabeçalho da coluna. */ /** Texto exibido no cabeçalho da coluna. */
rotulo: string; rotulo: string;
/** Função responsável por renderizar o conteúdo da célula. */ /** Função responsável por renderizar o conteúdo da célula. */
celula: (linha: T) => ComponenteCelula; celula: (linha: T) => ComponenteCelula;
/** Ação opcional disparada ao clicar na célula. */ /** Ação opcional disparada ao clicar na célula. */
acao?: () => void;
/** Alinhamento do conteúdo da coluna (cabeçalho e células). */
alinhamento?: EliAlinhamentoColuna;
/**
* Quando `true`, tenta truncar (ellipsis) conteúdos textuais longos.
* Observação: o tooltip automático é aplicado quando o conteúdo renderizado
* da célula é um `string`/`number`.
*/
truncar?: boolean;
/**
* Largura máxima usada quando `truncar` estiver ativo.
* Exemplos: `240` (px), `"18rem"`, `"30ch"`.
*/
largura_maxima?: number | string;
/** /**
* Campo de ordenação associado à coluna. Caso informado, a coluna passa a * Campo de ordenação associado à coluna. Caso informado, a coluna passa a
* exibir controles de ordenação e utiliza o valor como chave para o backend. * exibir controles de ordenação e utiliza o valor como chave para o backend.
*/ */
coluna_ordem?: keyof T; coluna_ordem?: keyof T;
/**
* indica que a coluna será visivel, se false incia em detalhe
* Caso tenha salvo a propriedade de visibilidade será adotado a propriedade salva
*/
visivel: boolean;
}; };
export type EliConsultaPaginada<T> = { export type EliConsultaPaginada<T> = {
/** Registros retornados na consulta. */ /** Registros retornados na consulta. */
@ -76,6 +69,7 @@ export type EliTabelaConsulta<T> = {
* ordenação (`coluna_ordem`/`direcao_ordem`) e paginação (`offSet`/`limit`). * ordenação (`coluna_ordem`/`direcao_ordem`) e paginação (`offSet`/`limit`).
*/ */
consulta: (parametrosConsulta?: { consulta: (parametrosConsulta?: {
filtros?: tipoFiltro[];
coluna_ordem?: keyof T; coluna_ordem?: keyof T;
direcao_ordem?: "asc" | "desc"; direcao_ordem?: "asc" | "desc";
offSet?: number; offSet?: number;
@ -103,4 +97,11 @@ export type EliTabelaConsulta<T> = {
/** Função executada ao clicar no botão. */ /** Função executada ao clicar no botão. */
acao: () => void; acao: () => void;
}[]; }[];
/** configuração para aplicação dos filtros padrões */
filtroAvancado?: {
rotulo: string;
coluna: keyof T;
operador: operadores | keyof typeof operadores;
entrada: ComponenteEntrada;
}[];
}; };

View file

@ -1,146 +0,0 @@
import { PropType } from "vue";
import type { CampoDensidade, CampoOpcao, CampoOpcaoBruta, CampoTipo, CampoValor, CampoValorMultiplo, CampoVariante } from "../../tipos";
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
/**
* Aceita valor simples (text-like) ou lista de valores (checkbox/select multiple).
* O componente não converte tipos automaticamente: mantém o que receber.
*/
modelValue: {
type: PropType<CampoValor | CampoValorMultiplo>;
default: string;
};
type: {
type: PropType<CampoTipo>;
default: string;
};
label: StringConstructor;
placeholder: StringConstructor;
disabled: BooleanConstructor;
error: BooleanConstructor;
errorMessages: {
type: PropType<string | string[]>;
default: () => never[];
};
hint: StringConstructor;
persistentHint: BooleanConstructor;
rows: {
type: NumberConstructor;
default: number;
};
/**
* Para select/radio/checkbox.
* Aceita lista normalizada ({ label, value }) ou valores primitivos.
*/
options: {
type: PropType<Array<CampoOpcaoBruta>>;
default: () => never[];
};
clearable: BooleanConstructor;
variant: {
type: PropType<CampoVariante>;
default: string;
};
density: {
type: PropType<CampoDensidade>;
default: string;
};
color: {
type: StringConstructor;
default: string;
};
row: BooleanConstructor;
showPasswordToggle: BooleanConstructor;
multiple: BooleanConstructor;
chips: BooleanConstructor;
}>, {
attrs: {
[x: string]: unknown;
};
value: import("vue").WritableComputedRef<CampoValor | CampoValorMultiplo, CampoValor | CampoValorMultiplo>;
isTextLike: import("vue").ComputedRef<boolean>;
inputHtmlType: import("vue").ComputedRef<"text" | "password">;
inputMode: import("vue").ComputedRef<"tel" | "decimal" | "numeric" | undefined>;
internalColor: import("vue").ComputedRef<string | undefined>;
showPassword: import("vue").Ref<boolean, boolean>;
togglePassword: () => void;
onInput: (e: Event) => void;
onFocus: () => void;
onBlur: () => void;
computedItems: import("vue").ComputedRef<CampoOpcao[]>;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "change" | "focus" | "blur")[], "update:modelValue" | "change" | "focus" | "blur", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
/**
* Aceita valor simples (text-like) ou lista de valores (checkbox/select multiple).
* O componente não converte tipos automaticamente: mantém o que receber.
*/
modelValue: {
type: PropType<CampoValor | CampoValorMultiplo>;
default: string;
};
type: {
type: PropType<CampoTipo>;
default: string;
};
label: StringConstructor;
placeholder: StringConstructor;
disabled: BooleanConstructor;
error: BooleanConstructor;
errorMessages: {
type: PropType<string | string[]>;
default: () => never[];
};
hint: StringConstructor;
persistentHint: BooleanConstructor;
rows: {
type: NumberConstructor;
default: number;
};
/**
* Para select/radio/checkbox.
* Aceita lista normalizada ({ label, value }) ou valores primitivos.
*/
options: {
type: PropType<Array<CampoOpcaoBruta>>;
default: () => never[];
};
clearable: BooleanConstructor;
variant: {
type: PropType<CampoVariante>;
default: string;
};
density: {
type: PropType<CampoDensidade>;
default: string;
};
color: {
type: StringConstructor;
default: string;
};
row: BooleanConstructor;
showPasswordToggle: BooleanConstructor;
multiple: BooleanConstructor;
chips: BooleanConstructor;
}>> & Readonly<{
"onUpdate:modelValue"?: ((...args: any[]) => any) | undefined;
onChange?: ((...args: any[]) => any) | undefined;
onFocus?: ((...args: any[]) => any) | undefined;
onBlur?: ((...args: any[]) => any) | undefined;
}>, {
color: string;
type: CampoTipo;
variant: CampoVariante;
disabled: boolean;
error: boolean;
persistentHint: boolean;
clearable: boolean;
row: boolean;
showPasswordToggle: boolean;
multiple: boolean;
chips: boolean;
modelValue: CampoValor | CampoValorMultiplo;
errorMessages: string | string[];
rows: number;
options: CampoOpcaoBruta[];
density: CampoDensidade;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
declare const _default: typeof __VLS_export;
export default _default;

View file

@ -1 +0,0 @@
export { default as EliInput } from "./EliInput.vue";

View file

@ -1 +0,0 @@
export declare function formatarCep(v: string): string;

View file

@ -1,9 +0,0 @@
export declare function somenteNumeros(valor: string): string;
export declare function formatarDecimal(valor: string): string;
/**
* Formatação para percentual:
* - remove '%' caso venha junto (ex: colar "10%")
* - mantém apenas dígitos e vírgula (no máximo uma)
*/
export declare function formatarPorcentagem(valor: string): string;
export declare function formatarMoeda(valor: string): string;

View file

@ -22,7 +22,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
}; };
}>, { }>, {
rotuloStatus: import("vue").ComputedRef<CartaoStatus>; rotuloStatus: import("vue").ComputedRef<CartaoStatus>;
corStatus: import("vue").ComputedRef<"primary" | "error" | "secondary" | "success">; corStatus: import("vue").ComputedRef<"primary" | "secondary" | "success" | "error">;
classeStatus: import("vue").ComputedRef<string>; classeStatus: import("vue").ComputedRef<string>;
onClick: () => void; onClick: () => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, { }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {

View file

@ -1,221 +0,0 @@
import { PropType } from "vue";
import type { CampoDensidade, CampoVariante } from "../../tipos";
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
/**
* Valor em ISO 8601:
* - com offset (ex.: `2026-01-09T13:15:00-03:00`)
* - ou UTC absoluto (ex.: `2026-01-09T16:15:00Z`)
*/
modelValue: {
type: PropType<string | null>;
default: null;
};
/**
* Define o tipo de entrada.
* - `dataHora`: usa `datetime-local`
* - `data`: usa `date`
*/
modo: {
type: PropType<"data" | "dataHora">;
default: string;
};
/** Rótulo exibido no v-text-field (Vuetify). */
rotulo: {
type: StringConstructor;
default: string;
};
/** Placeholder do input. */
placeholder: {
type: StringConstructor;
default: string;
};
/** Desabilita a interação. */
desabilitado: {
type: BooleanConstructor;
default: boolean;
};
/** Se true, mostra ícone para limpar o valor (Vuetify clearable). */
limpavel: {
type: BooleanConstructor;
default: boolean;
};
/** Estado de erro (visual). */
erro: {
type: BooleanConstructor;
default: boolean;
};
/** Mensagens de erro. */
mensagensErro: {
type: PropType<string | string[]>;
default: () => never[];
};
/** Texto de apoio. */
dica: {
type: StringConstructor;
default: string;
};
/** Mantém a dica sempre visível. */
dicaPersistente: {
type: BooleanConstructor;
default: boolean;
};
/** Densidade do campo (Vuetify). */
densidade: {
type: PropType<CampoDensidade>;
default: string;
};
/** Variante do v-text-field (Vuetify). */
variante: {
type: PropType<CampoVariante>;
default: string;
};
/**
* Valor mínimo permitido.
* ISO 8601 (offset ou `Z`).
*/
min: {
type: PropType<string | undefined>;
default: undefined;
};
/**
* Valor máximo permitido.
* ISO 8601 (offset ou `Z`).
*/
max: {
type: PropType<string | undefined>;
default: undefined;
};
}>, {
attrs: {
[x: string]: unknown;
};
valor: import("vue").WritableComputedRef<string, string>;
emit: ((event: "update:modelValue", _valor: string | null) => void) & ((event: "alterar", _valor: string | null) => void) & ((event: "foco") => void) & ((event: "desfoco") => void);
minLocal: import("vue").ComputedRef<string | undefined>;
maxLocal: import("vue").ComputedRef<string | undefined>;
tipoInput: import("vue").ComputedRef<"date" | "datetime-local">;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
/** v-model padrão. */
"update:modelValue": (_valor: string | null) => true;
/** Alias para consumidores que querem um evento semântico. */
alterar: (_valor: string | null) => true;
foco: () => true;
desfoco: () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
/**
* Valor em ISO 8601:
* - com offset (ex.: `2026-01-09T13:15:00-03:00`)
* - ou UTC absoluto (ex.: `2026-01-09T16:15:00Z`)
*/
modelValue: {
type: PropType<string | null>;
default: null;
};
/**
* Define o tipo de entrada.
* - `dataHora`: usa `datetime-local`
* - `data`: usa `date`
*/
modo: {
type: PropType<"data" | "dataHora">;
default: string;
};
/** Rótulo exibido no v-text-field (Vuetify). */
rotulo: {
type: StringConstructor;
default: string;
};
/** Placeholder do input. */
placeholder: {
type: StringConstructor;
default: string;
};
/** Desabilita a interação. */
desabilitado: {
type: BooleanConstructor;
default: boolean;
};
/** Se true, mostra ícone para limpar o valor (Vuetify clearable). */
limpavel: {
type: BooleanConstructor;
default: boolean;
};
/** Estado de erro (visual). */
erro: {
type: BooleanConstructor;
default: boolean;
};
/** Mensagens de erro. */
mensagensErro: {
type: PropType<string | string[]>;
default: () => never[];
};
/** Texto de apoio. */
dica: {
type: StringConstructor;
default: string;
};
/** Mantém a dica sempre visível. */
dicaPersistente: {
type: BooleanConstructor;
default: boolean;
};
/** Densidade do campo (Vuetify). */
densidade: {
type: PropType<CampoDensidade>;
default: string;
};
/** Variante do v-text-field (Vuetify). */
variante: {
type: PropType<CampoVariante>;
default: string;
};
/**
* Valor mínimo permitido.
* ISO 8601 (offset ou `Z`).
*/
min: {
type: PropType<string | undefined>;
default: undefined;
};
/**
* Valor máximo permitido.
* ISO 8601 (offset ou `Z`).
*/
max: {
type: PropType<string | undefined>;
default: undefined;
};
}>> & Readonly<{
"onUpdate:modelValue"?: ((_valor: string | null) => any) | undefined;
onAlterar?: ((_valor: string | null) => any) | undefined;
onFoco?: (() => any) | undefined;
onDesfoco?: (() => any) | undefined;
}>, {
placeholder: string;
modelValue: string | null;
modo: "data" | "dataHora";
rotulo: string;
desabilitado: boolean;
limpavel: boolean;
erro: boolean;
mensagensErro: string | string[];
dica: string;
dicaPersistente: boolean;
densidade: CampoDensidade;
variante: CampoVariante;
min: string | undefined;
max: string | undefined;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
/**
* EliDataHora
*
* Campo para entrada de data + hora.
*
* Modelo:
* - O componente **recebe** `modelValue` em ISO 8601 (UTC `Z` ou com offset)
* - Converte para horário local para exibir (`date` ou `datetime-local`)
* - Ao editar, **emite** ISO 8601 com o **offset local**
*/
declare const _default: typeof __VLS_export;
export default _default;

View file

@ -1 +0,0 @@
export { default as EliDataHora } from "./EliDataHora.vue";

View file

@ -1,18 +1,9 @@
type Habilidade = "vue" | "react";
declare const __VLS_export: import("vue").DefineComponent<{}, { declare const __VLS_export: import("vue").DefineComponent<{}, {
nome: import("vue").Ref<string, string>; nome: import("vue").Ref<string, string>;
email: import("vue").Ref<string, string>; email: import("vue").Ref<string, string>;
documento: import("vue").Ref<string, string>; documento: import("vue").Ref<string, string>;
estado: import("vue").Ref<string[], string[]>;
telefone: import("vue").Ref<string, string>; telefone: import("vue").Ref<string, string>;
mensagem: import("vue").Ref<string, string>;
senha: import("vue").Ref<string, string>;
cor: import("vue").Ref<"azul" | "verde" | null, "azul" | "verde" | null>;
habilidades: import("vue").Ref<Habilidade[], Habilidade[]>;
idade: import("vue").Ref<string, string>;
altura: import("vue").Ref<string, string>;
cep: import("vue").Ref<string, string>; cep: import("vue").Ref<string, string>;
valor: import("vue").Ref<string, string>;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, { }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {
EliBotao: import("vue").DefineComponent<import("vue").ExtractPropTypes<{ EliBotao: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
color: { color: {
@ -144,131 +135,59 @@ declare const __VLS_export: import("vue").DefineComponent<{}, {
badge: string | number | undefined; badge: string | number | undefined;
radius: import("../../tipos/indicador.js").IndicadorPresetRaio | import("../../tipos/indicador.js").CssLength; radius: import("../../tipos/indicador.js").IndicadorPresetRaio | import("../../tipos/indicador.js").CssLength;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>; }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
EliInput: import("vue").DefineComponent<import("vue").ExtractPropTypes<{ EliEntradaTexto: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
modelValue: { value: {
type: import("vue").PropType<import("../../tipos/campo.js").CampoValor | import("../../tipos/campo.js").CampoValorMultiplo>; type: import("vue").PropType<string | null | undefined>;
default: string; default: undefined;
}; };
type: { opcoes: {
type: import("vue").PropType<import("../../tipos/campo.js").CampoTipo>; type: import("vue").PropType<{
default: string; rotulo: string;
placeholder?: string;
} & {
limiteCaracteres?: number;
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep";
}>;
required: true;
}; };
label: StringConstructor;
placeholder: StringConstructor;
disabled: BooleanConstructor;
error: BooleanConstructor;
errorMessages: {
type: import("vue").PropType<string | string[]>;
default: () => never[];
};
hint: StringConstructor;
persistentHint: BooleanConstructor;
rows: {
type: NumberConstructor;
default: number;
};
options: {
type: import("vue").PropType<Array<import("../../tipos/campo.js").CampoOpcaoBruta>>;
default: () => never[];
};
clearable: BooleanConstructor;
variant: {
type: import("vue").PropType<import("../../tipos/campo.js").CampoVariante>;
default: string;
};
density: {
type: import("vue").PropType<import("../../tipos/campo.js").CampoDensidade>;
default: string;
};
color: {
type: StringConstructor;
default: string;
};
row: BooleanConstructor;
showPasswordToggle: BooleanConstructor;
multiple: BooleanConstructor;
chips: BooleanConstructor;
}>, { }>, {
attrs: { attrs: {
[x: string]: unknown; [x: string]: unknown;
}; };
value: import("vue").WritableComputedRef<import("../../tipos/campo.js").CampoValor | import("../../tipos/campo.js").CampoValorMultiplo, import("../../tipos/campo.js").CampoValor | import("../../tipos/campo.js").CampoValorMultiplo>; emit: ((event: "update:value", _v: string | null | undefined) => void) & ((event: "input", _v: string | null | undefined) => void) & ((event: "change", _v: string | null | undefined) => void) & ((event: "focus") => void) & ((event: "blur") => void);
isTextLike: import("vue").ComputedRef<boolean>; localValue: import("vue").WritableComputedRef<string | null | undefined, string | null | undefined>;
inputHtmlType: import("vue").ComputedRef<"text" | "password">; inputHtmlType: import("vue").ComputedRef<"text" | "email" | "url">;
inputMode: import("vue").ComputedRef<"tel" | "decimal" | "numeric" | undefined>; inputMode: import("vue").ComputedRef<string | undefined>;
internalColor: import("vue").ComputedRef<string | undefined>;
showPassword: import("vue").Ref<boolean, boolean>;
togglePassword: () => void;
onInput: (e: Event) => void; onInput: (e: Event) => void;
onFocus: () => void; }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
onBlur: () => void; "update:value": (_v: string | null | undefined) => true;
computedItems: import("vue").ComputedRef<import("../../tipos/campo.js").CampoOpcao[]>; input: (_v: string | null | undefined) => true;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "change" | "focus" | "blur")[], "update:modelValue" | "change" | "focus" | "blur", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{ change: (_v: string | null | undefined) => true;
modelValue: { focus: () => true;
type: import("vue").PropType<import("../../tipos/campo.js").CampoValor | import("../../tipos/campo.js").CampoValorMultiplo>; blur: () => true;
default: string; }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
value: {
type: import("vue").PropType<string | null | undefined>;
default: undefined;
}; };
type: { opcoes: {
type: import("vue").PropType<import("../../tipos/campo.js").CampoTipo>; type: import("vue").PropType<{
default: string; rotulo: string;
placeholder?: string;
} & {
limiteCaracteres?: number;
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep";
}>;
required: true;
}; };
label: StringConstructor;
placeholder: StringConstructor;
disabled: BooleanConstructor;
error: BooleanConstructor;
errorMessages: {
type: import("vue").PropType<string | string[]>;
default: () => never[];
};
hint: StringConstructor;
persistentHint: BooleanConstructor;
rows: {
type: NumberConstructor;
default: number;
};
options: {
type: import("vue").PropType<Array<import("../../tipos/campo.js").CampoOpcaoBruta>>;
default: () => never[];
};
clearable: BooleanConstructor;
variant: {
type: import("vue").PropType<import("../../tipos/campo.js").CampoVariante>;
default: string;
};
density: {
type: import("vue").PropType<import("../../tipos/campo.js").CampoDensidade>;
default: string;
};
color: {
type: StringConstructor;
default: string;
};
row: BooleanConstructor;
showPasswordToggle: BooleanConstructor;
multiple: BooleanConstructor;
chips: BooleanConstructor;
}>> & Readonly<{ }>> & Readonly<{
"onUpdate:modelValue"?: ((...args: any[]) => any) | undefined; "onUpdate:value"?: ((_v: string | null | undefined) => any) | undefined;
onChange?: ((...args: any[]) => any) | undefined; onInput?: ((_v: string | null | undefined) => any) | undefined;
onFocus?: ((...args: any[]) => any) | undefined; onChange?: ((_v: string | null | undefined) => any) | undefined;
onBlur?: ((...args: any[]) => any) | undefined; onFocus?: (() => any) | undefined;
onBlur?: (() => any) | undefined;
}>, { }>, {
color: string; value: string | null | undefined;
type: import("../../tipos/campo.js").CampoTipo;
variant: import("../../tipos/campo.js").CampoVariante;
disabled: boolean;
error: boolean;
persistentHint: boolean;
clearable: boolean;
row: boolean;
showPasswordToggle: boolean;
multiple: boolean;
chips: boolean;
modelValue: import("../../tipos/campo.js").CampoValor | import("../../tipos/campo.js").CampoValorMultiplo;
errorMessages: string | string[];
rows: number;
options: import("../../tipos/campo.js").CampoOpcaoBruta[];
density: import("../../tipos/campo.js").CampoDensidade;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>; }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>; }, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
declare const _default: typeof __VLS_export; declare const _default: typeof __VLS_export;

View file

@ -3,16 +3,14 @@ import "./styles/eli-vue-fonts.css";
import { EliOlaMundo } from "./componentes/ola_mundo"; import { EliOlaMundo } from "./componentes/ola_mundo";
import { EliBotao } from "./componentes/botao"; import { EliBotao } from "./componentes/botao";
import { EliBadge } from "./componentes/indicador"; import { EliBadge } from "./componentes/indicador";
import { EliInput } from "./componentes/campo";
import { EliCartao } from "./componentes/cartao"; import { EliCartao } from "./componentes/cartao";
import { EliDataHora } from "./componentes/data_hora"; import { EliTabela } from "./componentes/EliTabela";
import { EliTabela } from "./components/eli/EliTabela"; import { EliEntradaTexto, EliEntradaNumero, EliEntradaDataHora } from "./componentes/EliEntrada";
export { EliOlaMundo }; export { EliOlaMundo };
export { EliBotao }; export { EliBotao };
export { EliBadge }; export { EliBadge };
export { EliInput };
export { EliCartao }; export { EliCartao };
export { EliDataHora };
export { EliTabela }; export { EliTabela };
export { EliEntradaTexto, EliEntradaNumero, EliEntradaDataHora };
declare const EliVue: Plugin; declare const EliVue: Plugin;
export default EliVue; export default EliVue;

View file

@ -1,19 +0,0 @@
/**
* Tipos do componente EliInput (campo).
*/
export type CampoValor = string | number | boolean | null;
export type CampoValorMultiplo = CampoValor[];
export type CampoOpcao<TValor extends CampoValor = CampoValor> = {
label: string;
value: TValor;
disabled?: boolean;
};
export type CampoOpcaoBruta<TValor extends CampoValor = CampoValor> = TValor | {
label?: string;
value: TValor;
disabled?: boolean;
};
export type CampoVariante = "outlined" | "filled" | "plain" | "solo" | "solo-filled" | "solo-inverted" | "underlined";
export type CampoDensidade = "default" | "comfortable" | "compact";
export type CampoTipoNumerico = "numericoInteiro" | "numericoDecimal" | "numericoMoeda" | "porcentagem";
export type CampoTipo = "text" | "password" | "email" | "search" | "url" | "textarea" | "radio" | "checkbox" | "telefone" | "cpfCnpj" | "cep" | "select" | CampoTipoNumerico;

9
dist/types/tipos/entrada.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
/**
* Tipos compartilhados para componentes de entrada (EliEntrada*).
*
* OBS: Estes tipos existiam anteriormente em `tipos/campo.ts` junto do componente
* `EliInput`. Como o `EliInput` foi removido, mantemos aqui apenas o que ainda
* é relevante para padronização visual/props do Vuetify.
*/
export type CampoVariante = "outlined" | "filled" | "plain" | "solo" | "solo-filled" | "solo-inverted" | "underlined";
export type CampoDensidade = "default" | "comfortable" | "compact";

View file

@ -1,4 +1,4 @@
export * from "./botao"; export * from "./botao";
export * from "./cartao"; export * from "./cartao";
export * from "./campo"; export * from "./entrada";
export * from "./indicador"; export * from "./indicador";

View file

@ -1,6 +1,6 @@
{ {
"name": "eli-vue", "name": "eli-vue",
"version": "0.1.48", "version": "0.1.71",
"private": false, "private": false,
"main": "./dist/eli-vue.umd.js", "main": "./dist/eli-vue.umd.js",
"module": "./dist/eli-vue.es.js", "module": "./dist/eli-vue.es.js",
@ -35,8 +35,12 @@
"vuetify": "^3.11.2" "vuetify": "^3.11.2"
}, },
"dependencies": { "dependencies": {
"cross-fetch": "^4.1.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"lucide-vue-next": "^0.563.0", "lucide-vue-next": "^0.563.0",
"p-respostas": "git+https://git2.idz.one/publico/_respostas.git" "p-comuns": "git+https://git2.idz.one/publico/_comuns.git",
"p-respostas": "git+https://git2.idz.one/publico/_respostas.git",
"uuid": "^13.0.0",
"zod": "^4.3.6"
} }
} }

40
pnpm-lock.yaml generated
View file

@ -8,15 +8,27 @@ importers:
.: .:
dependencies: dependencies:
cross-fetch:
specifier: ^4.1.0
version: 4.1.0
dayjs: dayjs:
specifier: ^1.11.19 specifier: ^1.11.19
version: 1.11.19 version: 1.11.19
lucide-vue-next: lucide-vue-next:
specifier: ^0.563.0 specifier: ^0.563.0
version: 0.563.0(vue@3.5.25(typescript@5.9.3)) version: 0.563.0(vue@3.5.25(typescript@5.9.3))
p-comuns:
specifier: git+https://git2.idz.one/publico/_comuns.git
version: git+https://git2.idz.one/publico/_comuns.git#d783fa12940a5b1bcafa5038bd1c49c3f5f9b7fc(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@13.0.0)(zod@4.3.6)
p-respostas: p-respostas:
specifier: git+https://git2.idz.one/publico/_respostas.git specifier: git+https://git2.idz.one/publico/_respostas.git
version: git+https://git2.idz.one/publico/_respostas.git#8c24d790ace7255404745dcbdf12c5396e8b9843(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@11.1.0) version: git+https://git2.idz.one/publico/_respostas.git#8c24d790ace7255404745dcbdf12c5396e8b9843(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@13.0.0)
uuid:
specifier: ^13.0.0
version: 13.0.0
zod:
specifier: ^4.3.6
version: 4.3.6
devDependencies: devDependencies:
'@mdi/font': '@mdi/font':
specifier: ^7.4.47 specifier: ^7.4.47
@ -671,8 +683,8 @@ packages:
resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==}
engines: {node: '>=4'} engines: {node: '>=4'}
uuid@11.1.0: uuid@13.0.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
hasBin: true hasBin: true
vite-plugin-vuetify@2.1.2: vite-plugin-vuetify@2.1.2:
@ -764,6 +776,9 @@ packages:
zod@4.1.4: zod@4.1.4:
resolution: {integrity: sha512-2YqJuWkU6IIK9qcE4k1lLLhyZ6zFw7XVRdQGpV97jEIZwTrscUw+DY31Xczd8nwaoksyJUIxCojZXwckJovWxA==} resolution: {integrity: sha512-2YqJuWkU6IIK9qcE4k1lLLhyZ6zFw7XVRdQGpV97jEIZwTrscUw+DY31Xczd8nwaoksyJUIxCojZXwckJovWxA==}
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
snapshots: snapshots:
'@babel/helper-string-parser@7.27.1': {} '@babel/helper-string-parser@7.27.1': {}
@ -1197,16 +1212,23 @@ snapshots:
dependencies: dependencies:
whatwg-url: 5.0.0 whatwg-url: 5.0.0
p-comuns@git+https://git2.idz.one/publico/_comuns.git#d783fa12940a5b1bcafa5038bd1c49c3f5f9b7fc(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@11.1.0)(zod@4.1.4): p-comuns@git+https://git2.idz.one/publico/_comuns.git#d783fa12940a5b1bcafa5038bd1c49c3f5f9b7fc(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@13.0.0)(zod@4.1.4):
dependencies: dependencies:
cross-fetch: 4.1.0 cross-fetch: 4.1.0
dayjs: 1.11.19 dayjs: 1.11.19
uuid: 11.1.0 uuid: 13.0.0
zod: 4.1.4 zod: 4.1.4
p-respostas@git+https://git2.idz.one/publico/_respostas.git#8c24d790ace7255404745dcbdf12c5396e8b9843(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@11.1.0): p-comuns@git+https://git2.idz.one/publico/_comuns.git#d783fa12940a5b1bcafa5038bd1c49c3f5f9b7fc(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@13.0.0)(zod@4.3.6):
dependencies: dependencies:
p-comuns: git+https://git2.idz.one/publico/_comuns.git#d783fa12940a5b1bcafa5038bd1c49c3f5f9b7fc(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@11.1.0)(zod@4.1.4) cross-fetch: 4.1.0
dayjs: 1.11.19
uuid: 13.0.0
zod: 4.3.6
p-respostas@git+https://git2.idz.one/publico/_respostas.git#8c24d790ace7255404745dcbdf12c5396e8b9843(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@13.0.0):
dependencies:
p-comuns: git+https://git2.idz.one/publico/_comuns.git#d783fa12940a5b1bcafa5038bd1c49c3f5f9b7fc(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@13.0.0)(zod@4.1.4)
zod: 4.1.4 zod: 4.1.4
transitivePeerDependencies: transitivePeerDependencies:
- cross-fetch - cross-fetch
@ -1287,7 +1309,7 @@ snapshots:
upath@2.0.1: {} upath@2.0.1: {}
uuid@11.1.0: {} uuid@13.0.0: {}
vite-plugin-vuetify@2.1.2(vite@6.4.1(@types/node@24.10.1)(sass@1.94.2))(vue@3.5.25(typescript@5.9.3))(vuetify@3.11.2): vite-plugin-vuetify@2.1.2(vite@6.4.1(@types/node@24.10.1)(sass@1.94.2))(vue@3.5.25(typescript@5.9.3))(vuetify@3.11.2):
dependencies: dependencies:
@ -1346,3 +1368,5 @@ snapshots:
webidl-conversions: 3.0.1 webidl-conversions: 3.0.1
zod@4.1.4: {} zod@4.1.4: {}
zod@4.3.6: {}

View file

@ -0,0 +1,220 @@
<template>
<div class="eli-data-hora">
<v-text-field
v-model="valor"
:type="tipoInput"
:label="opcoesEfetivas.rotulo"
:placeholder="opcoesEfetivas.placeholder"
:disabled="desabilitadoEfetivo"
:clearable="Boolean(opcoesEfetivas.limpavel)"
:error="Boolean(opcoesEfetivas.erro)"
:error-messages="opcoesEfetivas.mensagensErro"
:hint="opcoesEfetivas.dica"
:persistent-hint="Boolean(opcoesEfetivas.dicaPersistente)"
:density="opcoesEfetivas.densidade ?? 'comfortable'"
:variant="opcoesEfetivas.variante ?? 'outlined'"
:min="minLocal"
:max="maxLocal"
v-bind="attrs"
@focus="emitCompatFocus"
@blur="emitCompatBlur"
/>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from "vue";
import dayjs from "dayjs";
import type { CampoDensidade, CampoVariante } from "../../tipos";
import type { PadroesEntradas } from "./tiposEntradas";
type EntradaDataHora = PadroesEntradas["dataHora"];
type PropsAntigas = {
modo?: "data" | "dataHora";
rotulo?: string;
placeholder?: string;
desabilitado?: boolean;
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
densidade?: CampoDensidade;
variante?: CampoVariante;
min?: string;
max?: string;
};
export default defineComponent({
name: "EliEntradaDataHora",
inheritAttrs: false,
props: {
// --- Novo padrão EliEntrada ---
value: {
type: String as PropType<EntradaDataHora["value"]>,
default: undefined,
},
opcoes: {
type: Object as PropType<EntradaDataHora["opcoes"]>,
required: false,
default: undefined,
},
// --- Compatibilidade com componente antigo EliDataHora ---
modelValue: {
type: String as PropType<string | null>,
default: null,
},
modo: { type: String as PropType<PropsAntigas["modo"]>, default: undefined },
rotulo: { type: String, default: undefined },
placeholder: { type: String, default: undefined },
desabilitado: { type: Boolean, default: undefined },
limpavel: { type: Boolean, default: undefined },
erro: { type: Boolean, default: undefined },
mensagensErro: {
type: [String, Array] as PropType<string | string[]>,
default: undefined,
},
dica: { type: String, default: undefined },
dicaPersistente: { type: Boolean, default: undefined },
densidade: { type: String as PropType<CampoDensidade>, default: undefined },
variante: { type: String as PropType<CampoVariante>, default: undefined },
min: { type: String as PropType<string | undefined>, default: undefined },
max: { type: String as PropType<string | undefined>, default: undefined },
},
emits: {
// Novo padrão
"update:value": (_v: string | null) => true,
input: (_v: string | null) => true, // compat Vue2
change: (_v: string | null) => true,
// Compat antigo
"update:modelValue": (_v: string | null) => true,
alterar: (_v: string | null) => true,
foco: () => true,
desfoco: () => true,
focus: () => true,
blur: () => true,
},
setup(props, { emit, attrs }) {
const opcoesEfetivas = computed<EntradaDataHora["opcoes"]>(() => {
// 1) se veio `opcoes` (novo), usa
if (props.opcoes) return props.opcoes;
// 2) fallback: constrói a partir das props antigas
return {
rotulo: props.rotulo ?? "Data e hora",
placeholder: props.placeholder ?? "",
modo: props.modo ?? "dataHora",
limpavel: props.limpavel,
erro: props.erro,
mensagensErro: props.mensagensErro,
dica: props.dica,
dicaPersistente: props.dicaPersistente,
densidade: props.densidade,
variante: props.variante,
min: props.min,
max: props.max,
};
});
const modoEfetivo = computed<"data" | "dataHora">(
() => opcoesEfetivas.value.modo ?? "dataHora"
);
const desabilitadoEfetivo = computed<boolean>(() => Boolean(props.desabilitado));
const tipoInput = computed<"date" | "datetime-local">(() =>
modoEfetivo.value === "data" ? "date" : "datetime-local"
);
function isoParaInputDatetime(valorIso: string): string {
if (modoEfetivo.value === "data") {
return dayjs(valorIso).format("YYYY-MM-DD");
}
return dayjs(valorIso).format("YYYY-MM-DDTHH:mm");
}
function inputDatetimeParaIsoLocal(valorInput: string): string {
if (modoEfetivo.value === "data") {
return dayjs(`${valorInput}T00:00`).format();
}
return dayjs(valorInput).format();
}
const effectiveModelValue = computed<string | null>(() => {
// Prioridade: value (novo) se vier definido; senão usa modelValue (antigo)
return props.value !== undefined ? (props.value ?? null) : props.modelValue;
});
const valor = computed<string>({
get: () => {
if (!effectiveModelValue.value) return "";
return isoParaInputDatetime(effectiveModelValue.value);
},
set: (v) => {
const normalizado = v && v.length > 0 ? v : null;
if (!normalizado) {
emit("update:value", null);
emit("input", null);
emit("change", null);
emit("update:modelValue", null);
emit("alterar", null);
return;
}
const valorEmitido = inputDatetimeParaIsoLocal(normalizado);
emit("update:value", valorEmitido);
emit("input", valorEmitido);
emit("change", valorEmitido);
emit("update:modelValue", valorEmitido);
emit("alterar", valorEmitido);
},
});
const minLocal = computed<string | undefined>(() => {
const min = opcoesEfetivas.value.min;
if (!min) return undefined;
return isoParaInputDatetime(min);
});
const maxLocal = computed<string | undefined>(() => {
const max = opcoesEfetivas.value.max;
if (!max) return undefined;
return isoParaInputDatetime(max);
});
function emitCompatFocus() {
emit("foco");
emit("focus");
}
function emitCompatBlur() {
emit("desfoco");
emit("blur");
}
return {
attrs,
valor,
tipoInput,
minLocal,
maxLocal,
opcoesEfetivas,
desabilitadoEfetivo,
emitCompatFocus,
emitCompatBlur,
};
},
});
</script>
<style scoped>
.eli-data-hora {
width: 100%;
}
</style>

View file

@ -0,0 +1,236 @@
<template>
<v-text-field
:model-value="displayValue"
:label="opcoes?.rotulo"
:placeholder="opcoes?.placeholder"
:type="isInteiro ? 'number' : 'text'"
:inputmode="isInteiro ? 'numeric' : 'decimal'"
:pattern="isInteiro ? '[0-9]*' : '[0-9.,]*'"
v-bind="attrs"
@update:model-value="onUpdateModelValue"
@focus="() => emit('focus')"
@blur="() => emit('blur')"
>
<!--
Em alguns cenários (ex.: type="number"), o prop `suffix` do Vuetify pode não aparecer.
Usamos slots para garantir exibição consistente de prefixo/sufixo.
-->
<template v-if="opcoes?.prefixo" #prepend-inner>
<span class="eli-entrada__prefixo">{{ opcoes.prefixo }}</span>
</template>
<template v-if="opcoes?.sufixo" #append-inner>
<span class="eli-entrada__sufixo">{{ opcoes.sufixo }}</span>
</template>
</v-text-field>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref, watch } from "vue";
import type { PadroesEntradas } from "./tiposEntradas";
type EntradaNumero = PadroesEntradas["numero"];
function casasDecimaisFromPrecisao(precisao: number): number {
if (!Number.isFinite(precisao) || precisao <= 0) return 0;
if (precisao >= 1) return 0;
// Preferência por contar casas decimais na representação (ex.: 0.01 -> 2)
const texto = precisao.toString();
if (texto.includes("e-")) {
const [, exp] = texto.split("e-");
const n = Number(exp);
return Number.isFinite(n) ? n : 0;
}
const idx = texto.indexOf(".");
if (idx === -1) return 0;
const dec = texto.slice(idx + 1).replace(/0+$/, "");
return dec.length;
}
function parseNumero(texto: string): number | null {
const normalizado = (texto ?? "").trim().replace(/,/g, ".");
if (!normalizado) return null;
const n = Number(normalizado);
if (Number.isNaN(n)) return null;
return n;
}
function formatarNumero(value: number | null | undefined, casas: number | null) {
if (value === null || value === undefined) return "";
if (casas === null) return String(value);
// Garantimos que o texto visual corresponda ao value (fixed).
const fixed = Number(value).toFixed(Math.max(0, casas));
// Exibe com vírgula (pt-BR)
return fixed.replace(/\./g, ",");
}
function somenteNumeros(texto: string) {
return (texto ?? "").replace(/\D+/g, "");
}
function somenteNumerosESeparadorDecimal(texto: string) {
// Mantém apenas números e UM separador decimal ("," ou "."), sem sinais.
// Implementação baseada em regex + colapso de separadores.
const limpo = (texto ?? "").replace(/[^0-9.,]+/g, "");
const matchSep = limpo.match(/[.,]/);
if (!matchSep) return limpo;
const sep = matchSep[0];
const idx = limpo.indexOf(sep);
const antesRaw = limpo.slice(0, idx).replace(/[.,]/g, "");
const depois = limpo.slice(idx + 1).replace(/[.,]/g, "");
// Se o usuário começa pelo separador (",4"), normalizamos para "0,4".
const antes = antesRaw.length ? antesRaw : "0";
return `${antes}${sep}${depois}`;
}
function limitarCasasDecimais(texto: string, casas: number | null) {
if (casas === null) return texto;
if (casas <= 0) return texto.replace(/[.,]/g, "");
const matchSep = texto.match(/[.,]/);
if (!matchSep) return texto;
const sep = matchSep[0];
const idx = texto.indexOf(sep);
const inteiro = texto.slice(0, idx);
const frac = texto.slice(idx + 1);
return `${inteiro}${sep}${frac.slice(0, casas)}`;
}
function valorParcialDecimal(texto: string): number | null {
// Regra (B): se o usuário digitou "1," ou "1." emite 1.
const m = texto.match(/^(\d+)[.,]$/);
if (!m) return null;
const n = Number(m[1]);
return Number.isNaN(n) ? null : n;
}
export default defineComponent({
name: "EliEntradaNumero",
inheritAttrs: false,
props: {
/** Interface padrão (EliEntrada): value + opcoes. */
value: {
type: [Number, null] as unknown as PropType<EntradaNumero["value"]>,
default: undefined,
},
opcoes: {
type: Object as PropType<EntradaNumero["opcoes"]>,
required: true,
},
},
emits: {
"update:value": (_v: EntradaNumero["value"]) => true,
/** Compat Vue2 (v-model padrão: value + input) */
input: (_v: EntradaNumero["value"]) => true,
change: (_v: EntradaNumero["value"]) => true,
focus: () => true,
blur: () => true,
},
setup(props, { attrs, emit }) {
// Se `precisao` não existir => não limitamos casas (mas ainda bloqueamos caracteres inválidos).
const casasDecimais = computed<number | null>(() => {
const p = props.opcoes?.precisao;
if (p === undefined || p === null) return null;
return casasDecimaisFromPrecisao(p);
});
const isInteiro = computed(() => {
// quando não existe precisão, tratamos como decimal livre
return casasDecimais.value === 0;
});
const isFixedPoint = computed(() => {
// Quando precisao existe e é < 1, controlamos a vírgula automaticamente.
const casas = casasDecimais.value;
return casas !== null && casas > 0;
});
// Controle do texto exibido: impede o campo de ficar com caracteres inválidos.
const displayValue = ref<string>("");
const ultimoEmitido = ref<EntradaNumero["value"] | undefined>(undefined);
watch(
() => props.value,
(v) => {
// Se foi uma atualização resultante do que acabamos de emitir, não sobrescreve.
// Isso evita o efeito de "apagar" o texto ao digitar algo parcial (ex.: "1,")
// quando o valor emitido é `1` ou `null`.
if (v === ultimoEmitido.value) return;
displayValue.value = formatarNumero(v as any, casasDecimais.value);
ultimoEmitido.value = v;
},
{ immediate: true }
);
function onUpdateModelValue(texto: string) {
// Modo fixed-point: usuário digita números continuamente e a vírgula é inserida automaticamente.
if (isFixedPoint.value) {
const casas = casasDecimais.value ?? 0;
const digitos = somenteNumeros(texto);
// ex.: casas=2, "1" => 0.01 ; "123" => 1.23
const baseInt = digitos ? Number(digitos) : 0;
const divisor = Math.pow(10, casas);
const n = digitos ? baseInt / divisor : null;
const out = (n === null ? null : n) as EntradaNumero["value"];
ultimoEmitido.value = out;
emit("update:value", out);
emit("input", out);
emit("change", out);
// display deve ser sempre o reflexo do value
displayValue.value = formatarNumero(out as any, casas);
return;
}
// Modo livre (sem precisao) ou inteiro: aceita números e 1 separador (no caso livre)
const base = isInteiro.value ? somenteNumeros(texto) : somenteNumerosESeparadorDecimal(texto);
const textoFiltrado = isInteiro.value ? base : limitarCasasDecimais(base, casasDecimais.value);
// Emissão:
// - vazio => null
// - decimal parcial ("1,") => 1 (regra B)
// - caso geral => parse normal
let out: EntradaNumero["value"] = null;
if (textoFiltrado) {
const parcial = isInteiro.value ? null : valorParcialDecimal(textoFiltrado);
const n = parcial ?? parseNumero(textoFiltrado);
out = (n === null ? null : n) as EntradaNumero["value"];
}
ultimoEmitido.value = out;
emit("update:value", out);
emit("input", out);
emit("change", out);
// display deve sempre corresponder ao value final
displayValue.value = formatarNumero(out as any, casasDecimais.value);
}
return { attrs, emit, displayValue, isInteiro, onUpdateModelValue };
},
});
</script>
<style scoped>
.eli-entrada__prefixo,
.eli-entrada__sufixo {
opacity: 0.75;
font-size: 0.9em;
white-space: nowrap;
}
.eli-entrada__prefixo {
margin-right: 6px;
}
.eli-entrada__sufixo {
margin-left: 6px;
}
</style>

View file

@ -0,0 +1,101 @@
<template>
<v-text-field
v-model="localValue"
:type="inputHtmlType"
:inputmode="inputMode"
:label="opcoes?.rotulo"
:placeholder="opcoes?.placeholder"
:counter="opcoes?.limiteCaracteres"
:maxlength="opcoes?.limiteCaracteres"
v-bind="attrs"
@focus="() => emit('focus')"
@blur="() => emit('blur')"
@input="onInput"
/>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from "vue";
import type { PadroesEntradas } from "./tiposEntradas";
import { formatarCpfCnpj } from "./utils/cpfCnpj";
import { formatTelefone } from "./utils/telefone";
import { formatarCep } from "./utils/cep";
type EntradaTexto = PadroesEntradas["texto"];
export default defineComponent({
name: "EliEntradaTexto",
inheritAttrs: false,
props: {
/** Interface padrão (EliEntrada): value + opcoes. */
value: {
type: [String, null] as unknown as PropType<EntradaTexto["value"]>,
default: undefined,
},
opcoes: {
type: Object as PropType<EntradaTexto["opcoes"]>,
required: true,
},
},
emits: {
"update:value": (_v: EntradaTexto["value"]) => true,
/** Compat Vue2 (v-model padrão: value + input) */
input: (_v: EntradaTexto["value"]) => true,
change: (_v: EntradaTexto["value"]) => true,
focus: () => true,
blur: () => true,
},
setup(props, { attrs, emit }) {
const formato = computed(() => props.opcoes?.formato ?? "texto");
const localValue = computed<EntradaTexto["value"]>({
get: () => props.value,
set: (v) => {
emit("update:value", v);
emit("input", v);
emit("change", v);
},
});
const inputHtmlType = computed(() => {
if (formato.value === "email") return "email";
if (formato.value === "url") return "url";
return "text";
});
const inputMode = computed<string | undefined>(() => {
if (formato.value === "telefone") return "tel";
if (formato.value === "cpfCnpj" || formato.value === "cep") return "numeric";
return undefined;
});
function aplicarFormato(valor: string) {
switch (formato.value) {
case "telefone":
return formatTelefone(valor);
case "cpfCnpj":
return formatarCpfCnpj(valor);
case "cep":
return formatarCep(valor);
default:
return valor;
}
}
function onInput(e: Event) {
const target = e.target as HTMLInputElement;
const resultado = aplicarFormato(target.value);
// garante que o input mostre o valor formatado
target.value = resultado;
// regra do projeto: value sempre igual ao que aparece
localValue.value = resultado;
}
return { attrs, emit, localValue, inputHtmlType, inputMode, onInput };
},
});
</script>
<style scoped></style>

View file

@ -0,0 +1,176 @@
# EliEntrada (Padrão de Entradas)
Esta pasta define o **padrão EliEntrada**: um conjunto de componentes de entrada (inputs) com uma **API uniforme**.
> TL;DR
> - Toda entrada recebe **`value`** (estado) e **`opcoes`** (configuração).
> - O padrão de uso é **`v-model:value`**.
> - Mantemos compatibilidade com Vue 2 via evento **`input`**.
---
## Para humanos (uso no dia-a-dia)
### Conceito
Um componente **EliEntrada** recebe **duas propriedades**:
- `value`: o valor atual do campo (entrada e saída)
- `opcoes`: um objeto que configura o componente (rótulo, placeholder e opções específicas do tipo)
Essa padronização facilita:
- gerar formulários dinamicamente
- trocar tipos de entrada com o mínimo de refactor
- documentar e tipar de forma previsível
### Tipos e contratos
Os contratos ficam em: [`tiposEntradas.ts`](./tiposEntradas.ts)
- `PadroesEntradas`: mapa de tipos suportados (ex.: `texto`, `numero`, `dataHora`)
- `TipoEntrada`: união das chaves do mapa (ex.: `"texto" | "numero" | "dataHora"`)
### Componentes disponíveis
#### 1) `EliEntradaTexto`
**Value**: `string | null | undefined`
**Opções** (além de `rotulo`/`placeholder`):
- `limiteCaracteres?: number`
Exemplo:
```vue
<template>
<EliEntradaTexto
v-model:value="nome"
:opcoes="{ rotulo: 'Nome', placeholder: 'Digite seu nome', limiteCaracteres: 50 }"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { EliEntradaTexto } from '@/index'
const nome = ref<string | null>(null)
</script>
```
---
#### 2) `EliEntradaNumero`
**Value**: `number | null | undefined`
**Opções**:
- `precisao?: number`
- `1` => inteiro
- `0.1` => 1 casa decimal
- `0.01` => 2 casas decimais
- `prefixo?: string` (ex.: `"R$"`)
- `sufixo?: string` (ex.: `"kg"`)
Comportamento:
- Quando `precisao < 1` o componente entra em modo **fixed-point**: você digita números continuamente e ele insere a vírgula automaticamente.
- O que é exibido sempre corresponde ao `value` emitido.
Exemplos:
```vue
<EliEntradaNumero
v-model:value="quantidade"
:opcoes="{ rotulo: 'Quantidade', placeholder: 'Ex: 10', precisao: 1, sufixo: 'kg' }"
/>
<EliEntradaNumero
v-model:value="preco"
:opcoes="{ rotulo: 'Preço', placeholder: 'Digite', precisao: 0.01, prefixo: 'R$' }"
/>
```
---
#### 3) `EliEntradaDataHora`
**Value**: `string | null | undefined` (ISO 8601 com offset ou `Z`)
**Opções**:
- `modo?: "data" | "dataHora"` (default: `dataHora`)
- `min?: string` (ISO)
- `max?: string` (ISO)
- `limpavel?: boolean`
- `erro?: boolean`
- `mensagensErro?: string | string[]`
- `dica?: string`
- `dicaPersistente?: boolean`
- `densidade?: CampoDensidade`
- `variante?: CampoVariante`
Importante:
- O input nativo `datetime-local` não carrega timezone.
- O componente converte ISO (Z/offset) para **local** para exibir.
- Ao alterar, emite ISO 8601 com o **offset local**.
Exemplo:
```vue
<EliEntradaDataHora
v-model:value="agendamento"
:opcoes="{ rotulo: 'Agendar', modo: 'dataHora', min, max, limpavel: true }"
/>
```
### Compatibilidade Vue 2 / Vue 3
Padrão recomendado (Vue 3):
- `v-model:value`
Compat Vue 2:
- todos os EliEntradas também emitem `input`.
- isso permite consumir com o padrão `value + input` quando necessário.
### Playground
- Entradas: `src/playground/entradas.playground.vue`
- Data/hora: `src/playground/data_hora.playground.vue`
---
## Para IA (contratos, invariantes e padrões de evolução)
### Contratos (não quebrar)
1) **Todo EliEntrada tem**:
- prop `value`
- prop `opcoes`
- evento `update:value`
2) **Compatibilidade**:
- emitir `input` (compat Vue 2) é obrigatório
3) **Tipagem**:
- `PadroesEntradas` é a fonte única do contrato (value/opcoes)
- `TipoEntrada = keyof PadroesEntradas`
4) **Sanitização/Normalização**:
- `EliEntradaNumero` deve bloquear caracteres inválidos e manter display coerente com `value`
- `EliEntradaDataHora` deve receber/emitir ISO e converter para local apenas para exibição
### Como adicionar uma nova entrada (checklist)
1) Adicionar chave em `PadroesEntradas` em `tiposEntradas.ts`
2) Criar `EliEntradaX.vue` seguindo o padrão:
- `value` + `opcoes`
- emite `update:value`, `input`, `change`
3) Exportar no `src/componentes/EliEntrada/index.ts`
4) Registrar no `src/componentes/EliEntrada/registryEliEntradas.ts`
5) Criar/atualizar playground (`src/playground/*.playground.vue`)
6) Validar `pnpm -s run build:types` e `pnpm -s run build`
### Padrões de mudança (refactors seguros)
- Se precisar mudar o contrato, faça **migração incremental**:
- manter props/eventos antigos como fallback temporário
- atualizar playground e exemplos
- rodar `build:types` para garantir geração de `.d.ts`

View file

@ -0,0 +1,6 @@
import EliEntradaTexto from "./EliEntradaTexto.vue";
import EliEntradaNumero from "./EliEntradaNumero.vue";
import EliEntradaDataHora from "./EliEntradaDataHora.vue";
export { EliEntradaTexto, EliEntradaNumero, EliEntradaDataHora };
export type { PadroesEntradas, TipoEntrada } from "./tiposEntradas";

View file

@ -0,0 +1,13 @@
import type { Component } from "vue";
import EliEntradaTexto from "./EliEntradaTexto.vue";
import EliEntradaNumero from "./EliEntradaNumero.vue";
import EliEntradaDataHora from "./EliEntradaDataHora.vue";
import type { TipoEntrada } from "./tiposEntradas";
export const registryTabelaCelulas = {
texto: EliEntradaTexto,
numero: EliEntradaNumero,
dataHora: EliEntradaDataHora,
} as const satisfies Record<TipoEntrada, Component>;

View file

@ -0,0 +1,132 @@
/**
* Tipos base para componentes de entrada (EliEntrada*)
*
* Objetivo:
* - Padronizar o shape de dados de todos os componentes de entrada.
* - Cada entrada possui sempre:
* 1) `value`: o valor atual (tipado)
* 2) `opcoes`: configuração do componente (rótulo, placeholder e extras por tipo)
*
* Como usar:
* - `PadroesEntradas[tipo]` retorna a tipagem completa (value + opcoes) daquele tipo.
* - `TipoEntrada` é a união com todos os tipos suportados (ex.: "texto" | "numero").
*/
/**
* Contrato padrão de uma entrada.
*
* @typeParam T - tipo do `value` (ex.: string | null | undefined)
* @typeParam Mais - campos adicionais dentro de `opcoes`, específicos do tipo de entrada
*/
export type tipoPadraoEntrada<T, Mais extends Record<string, unknown> = {}> = {
/** Valor atual do campo (pode aceitar null/undefined quando aplicável) */
value: T
/** Configurações do componente (visuais + regras simples do tipo) */
opcoes: {
/** Rótulo exibido ao usuário */
rotulo: string
/** Texto de ajuda dentro do input quando vazio */
placeholder?: string
} & Mais
}
/**
* Mapa de tipos de entrada suportados e suas configurações específicas.
*
* Observação importante:
* - As chaves deste objeto (ex.: "texto", "numero") viram o tipo `TipoEntrada`.
* - Cada item define:
* - `value`: tipo do valor
* - `opcoes`: opções comuns + extras específicas
*/
export type PadroesEntradas = {
texto: tipoPadraoEntrada<
string | null | undefined,
{
/** Limite máximo de caracteres permitidos (se definido) */
limiteCaracteres?: number
/**
* Formato/máscara aplicada ao texto.
* Obs: o `value` SEMPRE será o texto formatado (o que aparece no input).
*/
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep"
}
>
numero: tipoPadraoEntrada<
number | null | undefined,
{
/** Unidade de medida (ex.: "kg", "m³") */
sufixo?: string
/** Moéda (ex.: "R$") */
prefixo?: string
/**
* Passo/precisão do valor numérico.
* - 1 => somente inteiros
* - 0.1 => 1 casa decimal
* - 0.01 => 2 casas decimais
*
* Dica: este conceito corresponde ao atributo HTML `step`.
*/
precisao?: number
}
>
dataHora: tipoPadraoEntrada<
string | null | undefined,
{
/** Define o tipo de entrada. - `dataHora`: datetime-local - `data`: date */
modo?: "data" | "dataHora"
/** Se true, mostra ícone para limpar o valor (Vuetify clearable). */
limpavel?: boolean
/** Estado de erro (visual). */
erro?: boolean
/** Mensagens de erro. */
mensagensErro?: string | string[]
/** Texto de apoio. */
dica?: string
/** Mantém a dica sempre visível. */
dicaPersistente?: boolean
/** Valor mínimo permitido (ISO 8601 - offset ou Z). */
min?: string
/** Valor máximo permitido (ISO 8601 - offset ou Z). */
max?: string
/** Densidade do campo (Vuetify). */
densidade?: import("../../tipos").CampoDensidade
/** Variante do v-text-field (Vuetify). */
variante?: import("../../tipos").CampoVariante
}
>
}
/**
* União dos tipos de entrada suportados.
* Ex.: "texto" | "numero"
*/
export type TipoEntrada = keyof PadroesEntradas
export type PadraoComponenteEntrada<T extends TipoEntrada> =
readonly [T, PadroesEntradas[T]['opcoes']]
export type ComponenteEntrada = {
[K in TipoEntrada]: PadraoComponenteEntrada<K>
}[TipoEntrada]

View file

@ -0,0 +1,10 @@
function somenteNumeros(valor: string) {
return valor.replace(/\D+/g, "");
}
/** Formata CEP no padrão 00000-000 */
export function formatarCep(valor: string) {
const digitos = somenteNumeros(valor);
if (!digitos) return "";
return digitos.replace(/^(\d{5})(\d)/, "$1-$2").slice(0, 9);
}

View file

@ -1,5 +1,3 @@
// utils/telefone.ts
/** /**
* Remove tudo que não é número * Remove tudo que não é número
*/ */

View file

@ -95,26 +95,7 @@
background: rgba(0, 0, 0, 0.03); background: rgba(0, 0, 0, 0.03);
} }
.eli-tabela__celula--esquerda {
text-align: left;
}
.eli-tabela__celula--centro {
text-align: center;
}
.eli-tabela__celula--direita {
text-align: right;
}
.eli-tabela__celula-conteudo {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: top;
}
.eli-tabela--erro { .eli-tabela--erro {
border: 1px solid rgba(220, 53, 69, 0.35); border: 1px solid rgba(220, 53, 69, 0.35);

View file

@ -13,20 +13,32 @@
<EliTabelaCabecalho <EliTabelaCabecalho
v-if="exibirBusca || temAcoesCabecalho" v-if="exibirBusca || temAcoesCabecalho"
:exibirBusca="exibirBusca" :exibirBusca="exibirBusca"
:exibirBotaoFiltroAvancado="exibirFiltroAvancado"
:valorBusca="valorBusca" :valorBusca="valorBusca"
:acoesCabecalho="acoesCabecalho" :acoesCabecalho="acoesCabecalho"
@buscar="atualizarBusca" @buscar="atualizarBusca"
@colunas="abrirModalColunas" @colunas="abrirModalColunas"
@filtroAvancado="abrirModalFiltro"
/> />
<EliTabelaModalColunas <EliTabelaModalColunas
:aberto="modalColunasAberto" :aberto="modalColunasAberto"
:rotulosColunas="rotulosColunas" :rotulosColunas="rotulosColunas"
:configInicial="configColunas" :configInicial="configColunas"
:colunas="tabela.colunas"
@fechar="fecharModalColunas" @fechar="fecharModalColunas"
@salvar="salvarModalColunas" @salvar="salvarModalColunas"
/> />
<EliTabelaModalFiltroAvancado
:aberto="modalFiltroAberto"
:filtrosBase="tabela.filtroAvancado ?? []"
:modelo="filtrosUi"
@fechar="fecharModalFiltro"
@limpar="limparFiltrosAvancados"
@salvar="salvarFiltrosAvancados"
/>
<table class="eli-tabela__table"> <table class="eli-tabela__table">
<EliTabelaHead <EliTabelaHead
:colunas="colunasEfetivas" :colunas="colunasEfetivas"
@ -42,7 +54,7 @@
:colunasInvisiveis="colunasInvisiveisEfetivas" :colunasInvisiveis="colunasInvisiveisEfetivas"
:temColunasInvisiveis="temColunasInvisiveis" :temColunasInvisiveis="temColunasInvisiveis"
:linhasExpandidas="linhasExpandidas" :linhasExpandidas="linhasExpandidas"
:linhas="linhas" :linhas="linhasPaginadas"
:temAcoes="temAcoes" :temAcoes="temAcoes"
:menuAberto="menuAberto" :menuAberto="menuAberto"
:possuiAcoes="possuiAcoes" :possuiAcoes="possuiAcoes"
@ -56,12 +68,12 @@
:menuAberto="menuAberto" :menuAberto="menuAberto"
:posicao="menuPopupPos" :posicao="menuPopupPos"
:acoes="menuAberto === null ? [] : acoesDisponiveisPorLinha(menuAberto)" :acoes="menuAberto === null ? [] : acoesDisponiveisPorLinha(menuAberto)"
:linha="menuAberto === null ? null : linhas[menuAberto]" :linha="menuAberto === null ? null : linhasPaginadas[menuAberto]"
@executar="({ acao, linha }) => { menuAberto = null; acao.acao(linha as never); }" @executar="({ acao, linha }) => { menuAberto = null; acao.acao(linha as never); }"
/> />
<EliTabelaPaginacao <EliTabelaPaginacao
v-if="totalPaginas > 1 && quantidade > 0" v-if="totalPaginas > 1 && quantidadeFiltrada > 0"
:pagina="paginaAtual" :pagina="paginaAtual"
:totalPaginas="totalPaginas" :totalPaginas="totalPaginas"
:maximoBotoes="tabela.maximo_botoes_paginacao" :maximoBotoes="tabela.maximo_botoes_paginacao"
@ -98,15 +110,24 @@ import EliTabelaBody from "./EliTabelaBody.vue";
import EliTabelaMenuAcoes from "./EliTabelaMenuAcoes.vue"; import EliTabelaMenuAcoes from "./EliTabelaMenuAcoes.vue";
import EliTabelaPaginacao from "./EliTabelaPaginacao.vue"; import EliTabelaPaginacao from "./EliTabelaPaginacao.vue";
import EliTabelaModalColunas from "./EliTabelaModalColunas.vue"; import EliTabelaModalColunas from "./EliTabelaModalColunas.vue";
import EliTabelaModalFiltroAvancado from "./EliTabelaModalFiltroAvancado.vue";
import type { EliColuna } from "./types-eli-tabela"; import type { EliColuna } from "./types-eli-tabela";
/** Tipos da configuração/contrato da tabela */ /** Tipos da configuração/contrato da tabela */
import type { EliTabelaConsulta } from "./types-eli-tabela"; import type { EliTabelaConsulta } from "./types-eli-tabela";
import type { tipoFiltro } from "./types-eli-tabela";
// operadores usados no tipo de configuração; o operador aplicado vem travado no filtroAvancado.
import { import {
carregarConfigColunas, carregarConfigColunas,
salvarConfigColunas, salvarConfigColunas,
type EliTabelaColunasConfig, type EliTabelaColunasConfig,
} from "./colunasStorage"; } from "./colunasStorage";
import {
carregarFiltroAvancado,
salvarFiltroAvancado,
limparFiltroAvancado,
} from "./filtroAvancadoStorage";
export default defineComponent({ export default defineComponent({
name: "EliTabela", name: "EliTabela",
inheritAttrs: false, inheritAttrs: false,
@ -119,6 +140,7 @@ export default defineComponent({
EliTabelaMenuAcoes, EliTabelaMenuAcoes,
EliTabelaPaginacao, EliTabelaPaginacao,
EliTabelaModalColunas, EliTabelaModalColunas,
EliTabelaModalFiltroAvancado,
}, },
props: { props: {
/** Configuração principal da tabela (colunas, consulta e ações) */ /** Configuração principal da tabela (colunas, consulta e ações) */
@ -150,6 +172,56 @@ export default defineComponent({
const colunaOrdenacao = ref<string | null>(null); const colunaOrdenacao = ref<string | null>(null);
const direcaoOrdenacao = ref<"asc" | "desc">("asc"); const direcaoOrdenacao = ref<"asc" | "desc">("asc");
/** Filtro avançado (config + estado modal) */
const modalFiltroAberto = ref(false);
type LinhaFiltroUI<T> = {
coluna: keyof T;
valor: any;
};
const filtrosUi = ref<Array<LinhaFiltroUI<any>>>(carregarFiltroAvancado<any>(props.tabela.nome) as any);
function abrirModalFiltro() {
modalFiltroAberto.value = true;
}
function fecharModalFiltro() {
modalFiltroAberto.value = false;
}
function limparFiltrosAvancados() {
filtrosUi.value = [];
limparFiltroAvancado(props.tabela.nome);
modalFiltroAberto.value = false;
if (paginaAtual.value !== 1) paginaAtual.value = 1;
}
function salvarFiltrosAvancados(novo: any[]) {
filtrosUi.value = (novo ?? []) as any;
salvarFiltroAvancado(props.tabela.nome, (novo ?? []) as any);
modalFiltroAberto.value = false;
if (paginaAtual.value !== 1) paginaAtual.value = 1;
}
const filtrosAvancadosAtivos = computed<tipoFiltro[]>(() => {
// Operador vem travado na definição (`tabela.filtroAvancado`).
const base = (props.tabela.filtroAvancado ?? []) as Array<{ coluna: string; operador: any }>;
return (filtrosUi.value ?? [])
.filter((f) => f && f.coluna !== undefined)
.map((f) => {
const b = base.find((x) => String(x.coluna) === String(f.coluna));
if (!b) return null;
return {
coluna: String(b.coluna),
operador: b.operador as any,
valor: (f as any).valor,
} as tipoFiltro;
})
.filter(Boolean) as tipoFiltro[];
});
/** Alias reativo da prop tabela */ /** Alias reativo da prop tabela */
const tabela = computed(() => props.tabela); const tabela = computed(() => props.tabela);
@ -171,11 +243,20 @@ export default defineComponent({
const colunasInvisiveisEfetivas = computed(() => { const colunasInvisiveisEfetivas = computed(() => {
const colunas = props.tabela.colunas as Array<EliColuna<any>>; const colunas = props.tabela.colunas as Array<EliColuna<any>>;
const invisiveisSet = new Set(configColunas.value.invisiveis ?? []);
const configTemDados =
(configColunas.value.visiveis?.length ?? 0) > 0 ||
(configColunas.value.invisiveis?.length ?? 0) > 0;
const invisiveisBaseRotulos = configTemDados
? configColunas.value.invisiveis ?? []
: colunas.filter((c) => c.visivel === false).map((c) => c.rotulo);
const invisiveisSet = new Set(invisiveisBaseRotulos);
const base = colunas.filter((c) => invisiveisSet.has(c.rotulo)); const base = colunas.filter((c) => invisiveisSet.has(c.rotulo));
// ordenação: usa a lista salva de invisíveis (se existir), senão segue ordem original // ordenação: usa a lista (salva ou derivada do default) e adiciona novas ao final
const ordemSalva = configColunas.value.invisiveis ?? []; const ordemSalva = invisiveisBaseRotulos;
const mapa = new Map<string, EliColuna<any>>(); const mapa = new Map<string, EliColuna<any>>();
for (const c of base) { for (const c of base) {
if (!mapa.has(c.rotulo)) mapa.set(c.rotulo, c); if (!mapa.has(c.rotulo)) mapa.set(c.rotulo, c);
@ -198,14 +279,25 @@ export default defineComponent({
const colunasEfetivas = computed(() => { const colunasEfetivas = computed(() => {
const colunas = props.tabela.colunas; const colunas = props.tabela.colunas;
const todosRotulos = rotulosColunas.value; const todosRotulos = rotulosColunas.value;
const invisiveisSet = new Set(configColunas.value.invisiveis ?? []);
// default: todas visiveis; so some se estiver explicitamente em invisiveis const configTemDados =
(configColunas.value.visiveis?.length ?? 0) > 0 ||
(configColunas.value.invisiveis?.length ?? 0) > 0;
const invisiveisBaseRotulos = configTemDados
? configColunas.value.invisiveis ?? []
: (props.tabela.colunas as Array<EliColuna<any>>)
.filter((c) => c.visivel === false)
.map((c) => c.rotulo);
const invisiveisSet = new Set(invisiveisBaseRotulos);
// base visiveis: remove invisiveis
const visiveisBaseRotulos = todosRotulos.filter((r) => !invisiveisSet.has(r)); const visiveisBaseRotulos = todosRotulos.filter((r) => !invisiveisSet.has(r));
const visiveisSet = new Set(visiveisBaseRotulos); const visiveisSet = new Set(visiveisBaseRotulos);
// aplica ordem salva; novas (sem definicao) entram no fim, respeitando ordem original // aplica ordem salva; novas (sem definicao) entram no fim, respeitando ordem original
const ordemSalva = configColunas.value.visiveis ?? []; const ordemSalva = configTemDados ? configColunas.value.visiveis ?? [] : [];
const ordemFinal: string[] = []; const ordemFinal: string[] = [];
for (const r of ordemSalva) { for (const r of ordemSalva) {
@ -259,20 +351,91 @@ export default defineComponent({
return 10; return 10;
}); });
/** Total de páginas calculado com base na quantidade */ function aplicarFiltroTexto(linhasIn: unknown[]) {
const q = (valorBusca.value ?? "").trim().toLowerCase();
if (!q) return linhasIn;
// filtro simples: stringifica o objeto
return linhasIn.filter((l) => JSON.stringify(l).toLowerCase().includes(q));
}
function compararOperador(operador: string, valorLinha: any, valorFiltro: any): boolean {
switch (operador) {
case "=":
return valorLinha == valorFiltro;
case "!=":
return valorLinha != valorFiltro;
case ">":
return Number(valorLinha) > Number(valorFiltro);
case ">=":
return Number(valorLinha) >= Number(valorFiltro);
case "<":
return Number(valorLinha) < Number(valorFiltro);
case "<=":
return Number(valorLinha) <= Number(valorFiltro);
case "like": {
const a = String(valorLinha ?? "").toLowerCase();
const b = String(valorFiltro ?? "").toLowerCase();
return a.includes(b);
}
case "in": {
// aceita "a,b,c" ou array
const arr = Array.isArray(valorFiltro)
? valorFiltro
: String(valorFiltro ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
return arr.includes(String(valorLinha));
}
case "isNull":
return valorLinha === null || valorLinha === undefined || valorLinha === "";
default:
return true;
}
}
function aplicarFiltroAvancado(linhasIn: unknown[]) {
const filtros = filtrosAvancadosAtivos.value;
if (!filtros.length) return linhasIn;
return linhasIn.filter((l: any) => {
return filtros.every((f) => {
const vLinha = l?.[f.coluna as any];
return compararOperador(String(f.operador), vLinha, (f as any).valor);
});
});
}
const linhasFiltradas = computed(() => {
const base = linhas.value ?? [];
return aplicarFiltroAvancado(aplicarFiltroTexto(base));
});
/** Quantidade agora segue a filtragem local */
const quantidadeFiltrada = computed(() => linhasFiltradas.value.length);
/** Total de páginas calculado com base no filtrado */
const totalPaginas = computed(() => { const totalPaginas = computed(() => {
const limite = registrosPorConsulta.value; const limite = registrosPorConsulta.value;
if (!limite || limite <= 0) return 1; if (!limite || limite <= 0) return 1;
const total = quantidade.value; const total = quantidadeFiltrada.value;
if (!total) return 1; if (!total) return 1;
return Math.max(1, Math.ceil(total / limite)); return Math.max(1, Math.ceil(total / limite));
}); });
const linhasPaginadas = computed(() => {
const limite = Math.max(1, registrosPorConsulta.value);
const offset = (paginaAtual.value - 1) * limite;
return linhasFiltradas.value.slice(offset, offset + limite);
});
/** Indica se existem ações por linha */ /** Indica se existem ações por linha */
const temAcoes = computed(() => (props.tabela.acoesLinha ?? []).length > 0); const temAcoes = computed(() => (props.tabela.acoesLinha ?? []).length > 0);
const exibirFiltroAvancado = computed(() => (props.tabela.filtroAvancado ?? []).length > 0);
/** Sequencial para evitar race conditions entre consultas */ /** Sequencial para evitar race conditions entre consultas */
let carregamentoSequencial = 0; let carregamentoSequencial = 0;
@ -431,8 +594,10 @@ export default defineComponent({
menuAberto.value = null; menuAberto.value = null;
linhasExpandidas.value = {}; linhasExpandidas.value = {};
// Em modo simulação (filtro local), sempre buscamos a lista completa.
// A paginação é aplicada APÓS a filtragem.
const limite = Math.max(1, registrosPorConsulta.value); const limite = Math.max(1, registrosPorConsulta.value);
const offset = (paginaAtual.value - 1) * limite; const offset = 0;
const parametrosConsulta: { const parametrosConsulta: {
coluna_ordem?: never; coluna_ordem?: never;
@ -442,12 +607,10 @@ export default defineComponent({
texto_busca?: string; texto_busca?: string;
} = { } = {
offSet: offset, offSet: offset,
limit: limite, limit: 999999,
}; };
if (valorBusca.value) { // texto_busca ficará somente para filtragem local.
parametrosConsulta.texto_busca = valorBusca.value;
}
if (colunaOrdenacao.value) { if (colunaOrdenacao.value) {
parametrosConsulta.coluna_ordem = colunaOrdenacao.value as never; parametrosConsulta.coluna_ordem = colunaOrdenacao.value as never;
@ -468,12 +631,12 @@ export default defineComponent({
} }
const valores = res.valor?.valores ?? []; const valores = res.valor?.valores ?? [];
const total = res.valor?.quantidade ?? valores.length; const total = valores.length;
linhas.value = valores; linhas.value = valores;
quantidade.value = total; quantidade.value = total;
const totalPaginasRecalculado = Math.max(1, Math.ceil((total || 0) / limite)); const totalPaginasRecalculado = Math.max(1, Math.ceil((quantidadeFiltrada.value || 0) / limite));
if (paginaAtual.value > totalPaginasRecalculado) { if (paginaAtual.value > totalPaginasRecalculado) {
paginaAtual.value = totalPaginasRecalculado; paginaAtual.value = totalPaginasRecalculado;
return; return;
@ -557,7 +720,10 @@ export default defineComponent({
/** Watch: mudança de página dispara nova consulta */ /** Watch: mudança de página dispara nova consulta */
watch(paginaAtual, (nova, antiga) => { watch(paginaAtual, (nova, antiga) => {
if (nova !== antiga) void carregar(); // paginação local não precisa recarregar
if (nova !== antiga) {
// noop
}
}); });
/** Watch: troca de configuração reseta estados e recarrega */ /** Watch: troca de configuração reseta estados e recarrega */
@ -569,7 +735,9 @@ export default defineComponent({
direcaoOrdenacao.value = "asc"; direcaoOrdenacao.value = "asc";
valorBusca.value = ""; valorBusca.value = "";
modalColunasAberto.value = false; modalColunasAberto.value = false;
modalFiltroAberto.value = false;
configColunas.value = carregarConfigColunas(props.tabela.nome); configColunas.value = carregarConfigColunas(props.tabela.nome);
filtrosUi.value = carregarFiltroAvancado<any>(props.tabela.nome) as any;
linhasExpandidas.value = {}; linhasExpandidas.value = {};
if (paginaAtual.value !== 1) { if (paginaAtual.value !== 1) {
paginaAtual.value = 1; paginaAtual.value = 1;
@ -605,6 +773,8 @@ export default defineComponent({
carregando, carregando,
erro, erro,
linhas, linhas,
linhasPaginadas,
quantidadeFiltrada,
quantidade, quantidade,
menuAberto, menuAberto,
valorBusca, valorBusca,
@ -615,6 +785,7 @@ export default defineComponent({
// computed // computed
exibirBusca, exibirBusca,
exibirFiltroAvancado,
acoesCabecalho, acoesCabecalho,
temAcoesCabecalho, temAcoesCabecalho,
temAcoes, temAcoes,
@ -628,9 +799,16 @@ export default defineComponent({
linhasExpandidas, linhasExpandidas,
abrirModalColunas, abrirModalColunas,
abrirModalFiltro,
fecharModalColunas, fecharModalColunas,
salvarModalColunas, salvarModalColunas,
modalFiltroAberto,
filtrosUi,
salvarFiltrosAvancados,
limparFiltrosAvancados,
fecharModalFiltro,
alternarLinhaExpandida, alternarLinhaExpandida,
// actions // actions

View file

@ -29,22 +29,8 @@
v-for="(coluna, j) in colunas" v-for="(coluna, j) in colunas"
:key="`td-${i}-${j}`" :key="`td-${i}-${j}`"
class="eli-tabela__td" class="eli-tabela__td"
:class="[
coluna.acao ? 'eli-tabela__td--clicavel' : undefined,
obterClasseAlinhamento(coluna.alinhamento),
]"
@click="coluna.acao ? () => coluna.acao?.() : undefined"
>
<span
v-if="Boolean(coluna.truncar)"
class="eli-tabela__celula-conteudo"
:style="coluna.largura_maxima ? { maxWidth: obterMaxWidth(coluna.largura_maxima) } : undefined"
:title="obterTooltipCelula(coluna.celula(linha as never))"
> >
<EliTabelaCelula :celula="(coluna.celula(linha as never) as never)" /> <EliTabelaCelula :celula="(coluna.celula(linha as never) as never)" />
</span>
<EliTabelaCelula v-else :celula="(coluna.celula(linha as never) as never)" />
</td> </td>
<td v-if="temAcoes" class="eli-tabela__td eli-tabela__td--acoes" :key="`td-${i}-acoes`"> <td v-if="temAcoes" class="eli-tabela__td eli-tabela__td--acoes" :key="`td-${i}-acoes`">
@ -145,40 +131,9 @@ export default defineComponent({
}, },
}, },
setup() { setup() {
function obterClasseAlinhamento(alinhamento?: string) {
if (alinhamento === "direita") return "eli-tabela__celula--direita";
if (alinhamento === "centro") return "eli-tabela__celula--centro";
return "eli-tabela__celula--esquerda";
}
function obterMaxWidth(largura?: number | string) {
if (largura === undefined || largura === null) return undefined;
return typeof largura === "number" ? `${largura}px` : String(largura);
}
function obterTooltipCelula(celula: unknown) {
if (!Array.isArray(celula)) return undefined;
const tipo = celula[0];
const dados = celula[1] as any;
if (tipo === "textoSimples") {
return typeof dados?.texto === "string" ? dados.texto : undefined;
}
if (tipo === "numero") {
return typeof dados?.numero === "number" ? String(dados.numero) : undefined;
}
return undefined;
}
return { return {
ChevronRight, ChevronRight,
ChevronDown, ChevronDown,
obterClasseAlinhamento,
obterMaxWidth,
obterTooltipCelula,
}; };
}, },
}); });

View file

@ -10,6 +10,15 @@
> >
Colunas Colunas
</button> </button>
<button
v-if="exibirBotaoFiltroAvancado"
type="button"
class="eli-tabela__acoes-cabecalho-botao eli-tabela__acoes-cabecalho-botao--filtro"
@click="emitFiltroAvancado"
>
Filtro
</button>
<EliTabelaCaixaDeBusca :modelo="valorBusca" @buscar="emitBuscar" /> <EliTabelaCaixaDeBusca :modelo="valorBusca" @buscar="emitBuscar" />
</div> </div>
@ -53,6 +62,11 @@ export default defineComponent({
required: false, required: false,
default: true, default: true,
}, },
exibirBotaoFiltroAvancado: {
type: Boolean,
required: false,
default: false,
},
valorBusca: { valorBusca: {
type: String, type: String,
required: true, required: true,
@ -76,6 +90,9 @@ export default defineComponent({
colunas() { colunas() {
return true; return true;
}, },
filtroAvancado() {
return true;
},
}, },
setup(props, { emit }) { setup(props, { emit }) {
const temAcoesCabecalho = computed(() => props.acoesCabecalho.length > 0); const temAcoesCabecalho = computed(() => props.acoesCabecalho.length > 0);
@ -88,7 +105,11 @@ export default defineComponent({
emit("colunas"); emit("colunas");
} }
return { temAcoesCabecalho, emitBuscar, emitColunas }; function emitFiltroAvancado() {
emit("filtroAvancado");
}
return { temAcoesCabecalho, emitBuscar, emitColunas, emitFiltroAvancado };
}, },
}); });
</script> </script>

View file

@ -7,10 +7,7 @@
v-for="(coluna, idx) in colunas" v-for="(coluna, idx) in colunas"
:key="`th-${idx}`" :key="`th-${idx}`"
class="eli-tabela__th" class="eli-tabela__th"
:class="[ :class="[isOrdenavel(coluna) ? 'eli-tabela__th--ordenavel' : undefined]"
isOrdenavel(coluna) ? 'eli-tabela__th--ordenavel' : undefined,
obterClasseAlinhamento(coluna.alinhamento),
]"
scope="col" scope="col"
> >
<button <button
@ -91,12 +88,6 @@ export default defineComponent({
return coluna?.coluna_ordem !== undefined && coluna?.coluna_ordem !== null; return coluna?.coluna_ordem !== undefined && coluna?.coluna_ordem !== null;
} }
function obterClasseAlinhamento(alinhamento?: string) {
if (alinhamento === "direita") return "eli-tabela__celula--direita";
if (alinhamento === "centro") return "eli-tabela__celula--centro";
return "eli-tabela__celula--esquerda";
}
function emitAlternarOrdenacao(chave: string) { function emitAlternarOrdenacao(chave: string) {
emit("alternarOrdenacao", chave); emit("alternarOrdenacao", chave);
} }
@ -105,7 +96,6 @@ export default defineComponent({
ArrowUp, ArrowUp,
ArrowDown, ArrowDown,
isOrdenavel, isOrdenavel,
obterClasseAlinhamento,
emitAlternarOrdenacao, emitAlternarOrdenacao,
}; };
}, },

View file

@ -74,6 +74,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType, ref, watch } from "vue"; import { defineComponent, PropType, ref, watch } from "vue";
import type { EliTabelaColunasConfig } from "./colunasStorage"; import type { EliTabelaColunasConfig } from "./colunasStorage";
import type { EliColuna } from "./types-eli-tabela";
type OrigemLista = "visiveis" | "invisiveis"; type OrigemLista = "visiveis" | "invisiveis";
@ -100,6 +101,10 @@ export default defineComponent({
type: Object as PropType<EliTabelaColunasConfig>, type: Object as PropType<EliTabelaColunasConfig>,
required: true, required: true,
}, },
colunas: {
type: Array as PropType<Array<EliColuna<any>>>,
required: true,
},
}, },
emits: { emits: {
fechar() { fechar() {
@ -115,9 +120,18 @@ export default defineComponent({
function sincronizarEstado() { function sincronizarEstado() {
const todos = props.rotulosColunas; const todos = props.rotulosColunas;
const invisiveisSet = new Set(props.configInicial.invisiveis ?? []); const configTemDados =
(props.configInicial.visiveis?.length ?? 0) > 0 ||
(props.configInicial.invisiveis?.length ?? 0) > 0;
const invisiveisPadraoSet = new Set(
props.colunas.filter((c) => c.visivel === false).map((c) => c.rotulo)
);
const invisiveisSet = configTemDados
? new Set(props.configInicial.invisiveis ?? [])
: invisiveisPadraoSet;
// default: todas visíveis
const baseVisiveis = todos.filter((r) => !invisiveisSet.has(r)); const baseVisiveis = todos.filter((r) => !invisiveisSet.has(r));
// ordenação: aplica ordem salva (visiveis) e adiciona novas ao final // ordenação: aplica ordem salva (visiveis) e adiciona novas ao final
@ -139,7 +153,7 @@ export default defineComponent({
} }
watch( watch(
() => [props.aberto, props.rotulosColunas, props.configInicial] as const, () => [props.aberto, props.rotulosColunas, props.configInicial, props.colunas] as const,
() => { () => {
if (props.aberto) sincronizarEstado(); if (props.aberto) sincronizarEstado();
}, },

View file

@ -0,0 +1,391 @@
<template>
<div v-if="aberto" class="eli-tabela-modal-filtro__overlay" role="presentation" @click.self="emitFechar">
<div class="eli-tabela-modal-filtro__modal" role="dialog" aria-modal="true" aria-label="Filtro avançado">
<header class="eli-tabela-modal-filtro__header">
<h3 class="eli-tabela-modal-filtro__titulo">Filtro avançado</h3>
<button type="button" class="eli-tabela-modal-filtro__fechar" aria-label="Fechar" @click="emitFechar">
×
</button>
</header>
<div class="eli-tabela-modal-filtro__conteudo">
<div v-if="!filtrosBase.length" class="eli-tabela-modal-filtro__vazio">
Nenhum filtro configurado na tabela.
</div>
<div v-else class="eli-tabela-modal-filtro__lista">
<div v-for="(linha, idx) in linhas" :key="String(linha.coluna)" class="eli-tabela-modal-filtro__linha">
<div class="eli-tabela-modal-filtro__entrada">
<component
:is="componenteEntrada(linha.entrada)"
v-model:value="linha.valor"
:opcoes="opcoesEntrada(linha.entrada)"
density="compact"
/>
</div>
<button
type="button"
class="eli-tabela-modal-filtro__remover"
title="Remover"
aria-label="Remover"
@click="remover(idx)"
>
×
</button>
</div>
</div>
<div class="eli-tabela-modal-filtro__acoes">
<select v-model="colunaParaAdicionar" class="eli-tabela-modal-filtro__select" :disabled="!opcoesParaAdicionar.length">
<option disabled value="">Selecione um filtro</option>
<option v-for="o in opcoesParaAdicionar" :key="String(o.coluna)" :value="String(o.coluna)">
{{ rotuloDoFiltro(o) }}
</option>
</select>
<button
type="button"
class="eli-tabela-modal-filtro__botao"
@click="adicionar"
:disabled="!colunaParaAdicionar"
>
Adicionar
</button>
</div>
</div>
<footer class="eli-tabela-modal-filtro__footer">
<button type="button" class="eli-tabela-modal-filtro__botao eli-tabela-modal-filtro__botao--sec" @click="emitLimpar">
Limpar
</button>
<button type="button" class="eli-tabela-modal-filtro__botao eli-tabela-modal-filtro__botao--sec" @click="emitFechar">
Cancelar
</button>
<button type="button" class="eli-tabela-modal-filtro__botao eli-tabela-modal-filtro__botao--prim" @click="emitSalvar">
Aplicar
</button>
</footer>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref, watch } from "vue";
import { EliEntradaTexto, EliEntradaNumero, EliEntradaDataHora } from "../EliEntrada";
import type { ComponenteEntrada, TipoEntrada } from "../EliEntrada/tiposEntradas";
import type { EliTabelaConsulta } from "./types-eli-tabela";
type FiltroBase<T> = NonNullable<EliTabelaConsulta<T>["filtroAvancado"]>[number];
type LinhaFiltro<T> = {
coluna: keyof T;
entrada: ComponenteEntrada;
operador: string;
valor: any;
};
function isTipoEntrada(v: unknown): v is TipoEntrada {
return v === "texto" || v === "numero" || v === "dataHora";
}
function rotuloDoFiltro(f: FiltroBase<any>) {
const rotulo = (f?.entrada?.[1] as any)?.rotulo;
return rotulo ? String(rotulo) : String(f?.coluna ?? "Filtro");
}
export default defineComponent({
name: "EliTabelaModalFiltroAvancado",
props: {
aberto: { type: Boolean, required: true },
filtrosBase: {
type: Array as PropType<Array<FiltroBase<any>>>,
required: true,
},
modelo: {
type: Array as PropType<Array<any>>,
required: true,
},
},
emits: {
fechar: () => true,
limpar: () => true,
salvar: (_linhas: any[]) => true,
},
setup(props, { emit }) {
const linhas = ref<Array<LinhaFiltro<any>>>([]);
const colunaParaAdicionar = ref<string>("");
const colunasDisponiveis = computed(() => (props.filtrosBase ?? []).map((b) => String(b.coluna)));
const opcoesParaAdicionar = computed(() => {
const usadas = new Set(linhas.value.map((l) => String(l.coluna)));
return (props.filtrosBase ?? []).filter((b) => !usadas.has(String(b.coluna)));
});
function componenteEntrada(entrada: ComponenteEntrada) {
const tipo = entrada?.[0];
if (tipo === "numero") return EliEntradaNumero;
if (tipo === "dataHora") return EliEntradaDataHora;
return EliEntradaTexto;
}
function opcoesEntrada(entrada: ComponenteEntrada) {
// o rótulo vem do próprio componente (entrada[1].rotulo)
return (entrada?.[1] as any) ?? { rotulo: "" };
}
function valorInicialPorEntrada(entrada: ComponenteEntrada) {
const tipo = entrada?.[0];
if (tipo === "numero") return null;
return "";
}
function normalizarModelo() {
const base = props.filtrosBase ?? [];
const modelo = Array.isArray(props.modelo) ? props.modelo : [];
linhas.value = modelo.map((m: any) => {
// operador vem travado no base
const baseItem = base.find((b) => String(b.coluna) === String(m.coluna)) ?? base[0];
const entrada = (baseItem?.entrada ?? m.entrada) as ComponenteEntrada;
const col = (baseItem?.coluna ?? m.coluna) as any;
const op = String(baseItem?.operador ?? "=");
const val = m.valor ?? valorInicialPorEntrada(entrada);
return {
coluna: col,
operador: op,
entrada,
valor: val,
} as LinhaFiltro<any>;
});
// se vazio e existe base, adiciona 1 linha default
// não auto-adiciona; usuário escolhe quais filtros quer usar
// se algum filtro mudou a coluna para valor inválido, ajusta
for (const l of linhas.value) {
if (!colunasDisponiveis.value.includes(String(l.coluna))) continue;
l.operador = String((base.find((b) => String(b.coluna) === String(l.coluna))?.operador ?? "="));
// sanity
if (l.entrada && !isTipoEntrada(l.entrada[0])) {
l.entrada = ["texto", { rotulo: "Valor" }] as any;
}
}
}
watch(
() => [props.aberto, props.filtrosBase, props.modelo] as const,
() => {
if (props.aberto) normalizarModelo();
},
{ deep: true, immediate: true }
);
function adicionar() {
if (!colunaParaAdicionar.value) return;
const b0 = (props.filtrosBase ?? []).find((b) => String(b.coluna) === String(colunaParaAdicionar.value));
if (!b0) return;
// evita repetição
if (linhas.value.some((l) => String(l.coluna) === String(b0.coluna))) return;
linhas.value.push({
coluna: b0.coluna as any,
entrada: b0.entrada,
operador: String(b0.operador ?? "="),
valor: valorInicialPorEntrada(b0.entrada),
});
colunaParaAdicionar.value = "";
}
function remover(idx: number) {
linhas.value.splice(idx, 1);
}
function emitFechar() {
emit("fechar");
}
function emitLimpar() {
emit("limpar");
}
function emitSalvar() {
emit(
"salvar",
linhas.value.map((l) => ({
coluna: l.coluna,
valor: l.valor,
}))
);
}
return {
linhas,
opcoesParaAdicionar,
colunaParaAdicionar,
componenteEntrada,
opcoesEntrada,
adicionar,
remover,
// exibimos operador fixo só como texto
emitFechar,
emitSalvar,
emitLimpar,
rotuloDoFiltro,
};
},
});
</script>
<style scoped>
.eli-tabela-modal-filtro__overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.35);
z-index: 4000;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.eli-tabela-modal-filtro__modal {
width: min(980px, 100%);
background: #fff;
border-radius: 14px;
border: 1px solid rgba(15, 23, 42, 0.1);
box-shadow: 0 18px 60px rgba(15, 23, 42, 0.25);
overflow: hidden;
}
.eli-tabela-modal-filtro__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
}
.eli-tabela-modal-filtro__titulo {
font-size: 1rem;
margin: 0;
}
.eli-tabela-modal-filtro__fechar {
width: 34px;
height: 34px;
border-radius: 10px;
border: none;
background: transparent;
cursor: pointer;
font-size: 22px;
line-height: 1;
color: rgba(15, 23, 42, 0.8);
}
.eli-tabela-modal-filtro__fechar:hover,
.eli-tabela-modal-filtro__fechar:focus-visible {
background: rgba(15, 23, 42, 0.06);
}
.eli-tabela-modal-filtro__conteudo {
padding: 16px;
}
.eli-tabela-modal-filtro__vazio {
opacity: 0.75;
font-size: 0.9rem;
}
.eli-tabela-modal-filtro__lista {
display: grid;
gap: 10px;
}
.eli-tabela-modal-filtro__linha {
display: grid;
grid-template-columns: 1fr 34px;
gap: 10px;
align-items: center;
}
.eli-tabela-modal-filtro__select {
height: 34px;
border-radius: 10px;
border: 1px solid rgba(15, 23, 42, 0.12);
padding: 0 10px;
background: #fff;
}
.eli-tabela-modal-filtro__entrada {
min-width: 0;
}
.eli-tabela-modal-filtro__remover {
width: 34px;
height: 34px;
border-radius: 10px;
border: 1px solid rgba(15, 23, 42, 0.12);
background: #fff;
cursor: pointer;
font-size: 18px;
line-height: 1;
color: rgba(15, 23, 42, 0.8);
}
.eli-tabela-modal-filtro__remover:hover,
.eli-tabela-modal-filtro__remover:focus-visible {
background: rgba(15, 23, 42, 0.06);
}
.eli-tabela-modal-filtro__acoes {
margin-top: 12px;
}
.eli-tabela-modal-filtro__footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 14px 16px;
border-top: 1px solid rgba(15, 23, 42, 0.08);
}
.eli-tabela-modal-filtro__botao {
height: 34px;
padding: 0 14px;
border-radius: 10px;
border: 1px solid rgba(15, 23, 42, 0.12);
background: #fff;
cursor: pointer;
font-size: 0.9rem;
}
.eli-tabela-modal-filtro__botao:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.eli-tabela-modal-filtro__botao--sec:hover,
.eli-tabela-modal-filtro__botao--sec:focus-visible {
background: rgba(15, 23, 42, 0.06);
}
.eli-tabela-modal-filtro__botao--prim {
border: none;
background: rgba(37, 99, 235, 0.95);
color: #fff;
}
.eli-tabela-modal-filtro__botao--prim:hover,
.eli-tabela-modal-filtro__botao--prim:focus-visible {
background: rgba(37, 99, 235, 1);
}
@media (max-width: 880px) {
.eli-tabela-modal-filtro__linha {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -0,0 +1,59 @@
<template>
<button
v-if="dados?.acao"
type="button"
class="eli-tabela__celula-link"
@click.stop.prevent="dados.acao()"
>
{{ String(dados?.numero).replace('.', ',') }}
</button>
<span v-else>{{ String(dados?.numero).replace('.', ',') }}</span>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue"
import type { TiposTabelaCelulas } from "./tiposTabelaCelulas";
export default defineComponent({
name: "EliTabelaCelulaNumero",
components: {},
props: {
dados: {
type: Object as PropType<TiposTabelaCelulas["numero"]>,
},
},
data() {
return {
}
},
methods: {
},
setup({ dados }) {
return { dados }
},
})
</script>
<style scoped>
.eli-tabela__celula-link {
all: unset;
display: inline;
color: #2563eb;
cursor: pointer;
text-decoration: underline;
text-decoration-color: rgba(37, 99, 235, 0.55);
text-underline-offset: 2px;
}
.eli-tabela__celula-link:hover {
color: #1d4ed8;
text-decoration-color: rgba(29, 78, 216, 0.75);
}
.eli-tabela__celula-link:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.45);
outline-offset: 2px;
border-radius: 4px;
}
</style>

View file

@ -0,0 +1,59 @@
<template>
<button
v-if="dados?.acao"
type="button"
class="eli-tabela__celula-link"
@click.stop.prevent="dados.acao()"
>
{{ dados?.texto }}
</button>
<span v-else>{{ dados?.texto }}</span>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue"
import type { TiposTabelaCelulas } from "./tiposTabelaCelulas";
export default defineComponent({
name: "EliTabelaCelulaTextoSimples",
components: {},
props: {
dados: {
type: Object as PropType<TiposTabelaCelulas["textoSimples"]>,
},
},
data() {
return {
}
},
methods: {
},
setup({ dados }) {
return { dados }
},
})
</script>
<style scoped>
.eli-tabela__celula-link {
all: unset;
display: inline;
color: #2563eb;
cursor: pointer;
text-decoration: underline;
text-decoration-color: rgba(37, 99, 235, 0.55);
text-underline-offset: 2px;
}
.eli-tabela__celula-link:hover {
color: #1d4ed8;
text-decoration-color: rgba(29, 78, 216, 0.75);
}
.eli-tabela__celula-link:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.45);
outline-offset: 2px;
border-radius: 4px;
}
</style>

View file

@ -0,0 +1,63 @@
<template>
<!-- TODO: Validar de ação está cehgando aqui-->
<button
v-if="dados?.acao"
type="button"
class="eli-tabela__texto-truncado eli-tabela__celula-link"
:title="dados?.texto"
@click.stop.prevent="dados.acao()"
>
{{ dados?.texto }}
</button>
<span v-else class="eli-tabela__texto-truncado" :title="dados?.texto">{{ dados?.texto }}</span>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import type { TiposTabelaCelulas } from "./tiposTabelaCelulas";
export default defineComponent({
name: "EliTabelaCelulaTextoTruncado",
props: {
dados: {
type: Object as PropType<TiposTabelaCelulas["textoTruncado"]>,
},
},
setup({ dados }) {
return { dados };
},
});
</script>
<style scoped>
.eli-tabela__texto-truncado {
display: inline-block;
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: top;
}
.eli-tabela__celula-link {
all: unset;
display: inline;
color: #2563eb;
cursor: pointer;
text-decoration: underline;
text-decoration-color: rgba(37, 99, 235, 0.55);
text-underline-offset: 2px;
}
.eli-tabela__celula-link:hover {
color: #1d4ed8;
text-decoration-color: rgba(29, 78, 216, 0.75);
}
.eli-tabela__celula-link:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.45);
outline-offset: 2px;
border-radius: 4px;
}
</style>

View file

@ -1,10 +1,12 @@
import type { Component } from "vue"; import type { Component } from "vue";
import EliTabelaCelulaTextoSimples from "./EliTabelaCelulaTextoSimples.vue"; import EliTabelaCelulaTextoSimples from "./EliTabelaCelulaTextoSimples.vue";
import EliTabelaCelulaTextoTruncado from "./EliTabelaCelulaTextoTruncado.vue";
import EliTabelaCelulaNumero from "./EliTabelaCelulaNumero.vue"; import EliTabelaCelulaNumero from "./EliTabelaCelulaNumero.vue";
import type { TipoTabelaCelula } from "./tiposTabelaCelulas"; import type { TipoTabelaCelula } from "./tiposTabelaCelulas";
export const registryTabelaCelulas = { export const registryTabelaCelulas = {
textoSimples: EliTabelaCelulaTextoSimples, textoSimples: EliTabelaCelulaTextoSimples,
textoTruncado: EliTabelaCelulaTextoTruncado,
numero: EliTabelaCelulaNumero, numero: EliTabelaCelulaNumero,
} as const satisfies Record<TipoTabelaCelula, Component>; } as const satisfies Record<TipoTabelaCelula, Component>;

View file

@ -7,6 +7,10 @@ export type TiposTabelaCelulas = {
texto: string; texto: string;
acao?: () => void; acao?: () => void;
}; };
textoTruncado: {
texto: string;
acao?: () => void;
};
numero: { numero: {
numero: number; numero: number;
acao?: () => void; acao?: () => void;
@ -14,3 +18,4 @@ export type TiposTabelaCelulas = {
}; };
export type TipoTabelaCelula = keyof TiposTabelaCelulas; export type TipoTabelaCelula = keyof TiposTabelaCelulas;

View file

@ -0,0 +1,35 @@
export type EliTabelaFiltroAvancadoSalvo<T> = Array<{
coluna: keyof T
valor: any
}>;
function key(nomeTabela: string) {
return `eli_tabela:${nomeTabela}:filtro_avancado`;
}
export function carregarFiltroAvancado<T>(nomeTabela: string): EliTabelaFiltroAvancadoSalvo<T> {
try {
const raw = localStorage.getItem(key(nomeTabela));
if (!raw) return [] as unknown as EliTabelaFiltroAvancadoSalvo<T>;
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? (parsed as EliTabelaFiltroAvancadoSalvo<T>) : ([] as any);
} catch {
return [] as unknown as EliTabelaFiltroAvancadoSalvo<T>;
}
}
export function salvarFiltroAvancado<T>(nomeTabela: string, filtros: EliTabelaFiltroAvancadoSalvo<T>) {
try {
localStorage.setItem(key(nomeTabela), JSON.stringify(filtros ?? []));
} catch {
// ignore
}
}
export function limparFiltroAvancado(nomeTabela: string) {
try {
localStorage.removeItem(key(nomeTabela));
} catch {
// ignore
}
}

View file

@ -1,6 +1,11 @@
import type { tipoResposta } from "p-respostas"; import type { tipoResposta } from "p-respostas";
import type { LucideIcon } from "lucide-vue-next"; import type { LucideIcon } from "lucide-vue-next";
import type { TipoTabelaCelula, TiposTabelaCelulas } from "./celulas/tiposTabelaCelulas"; import type { TipoTabelaCelula, TiposTabelaCelulas } from "./celulas/tiposTabelaCelulas";
import { operadores, zFiltro } from "p-comuns";
import { ComponenteEntrada } from "../EliEntrada/tiposEntradas";
// `p-comuns` expõe `zFiltro` (schema). Inferimos o tipo a partir do `parse`.
export type tipoFiltro = ReturnType<(typeof zFiltro)["parse"]>
@ -24,7 +29,6 @@ export type { TipoTabelaCelula, TiposTabelaCelulas };
export type EliAlinhamentoColuna = "esquerda" | "centro" | "direita";
export type EliColuna<T> = { export type EliColuna<T> = {
/** Texto exibido no cabeçalho da coluna. */ /** Texto exibido no cabeçalho da coluna. */
@ -32,25 +36,18 @@ export type EliColuna<T> = {
/** Função responsável por renderizar o conteúdo da célula. */ /** Função responsável por renderizar o conteúdo da célula. */
celula: (linha: T) => ComponenteCelula; celula: (linha: T) => ComponenteCelula;
/** Ação opcional disparada ao clicar na célula. */ /** Ação opcional disparada ao clicar na célula. */
acao?: () => void;
/** Alinhamento do conteúdo da coluna (cabeçalho e células). */
alinhamento?: EliAlinhamentoColuna;
/**
* Quando `true`, tenta truncar (ellipsis) conteúdos textuais longos.
* Observação: o tooltip automático é aplicado quando o conteúdo renderizado
* da célula é um `string`/`number`.
*/
truncar?: boolean;
/**
* Largura máxima usada quando `truncar` estiver ativo.
* Exemplos: `240` (px), `"18rem"`, `"30ch"`.
*/
largura_maxima?: number | string;
/** /**
* Campo de ordenação associado à coluna. Caso informado, a coluna passa a * Campo de ordenação associado à coluna. Caso informado, a coluna passa a
* exibir controles de ordenação e utiliza o valor como chave para o backend. * exibir controles de ordenação e utiliza o valor como chave para o backend.
*/ */
coluna_ordem?: keyof T; coluna_ordem?: keyof T;
/**
* indica que a coluna será visivel, se false incia em detalhe
* Caso tenha salvo a propriedade de visibilidade será adotado a propriedade salva
*/
visivel: boolean
}; };
export type EliConsultaPaginada<T> = { export type EliConsultaPaginada<T> = {
@ -98,6 +95,10 @@ export type EliTabelaConsulta<T> = {
* ordenação (`coluna_ordem`/`direcao_ordem`) e paginação (`offSet`/`limit`). * ordenação (`coluna_ordem`/`direcao_ordem`) e paginação (`offSet`/`limit`).
*/ */
consulta: (parametrosConsulta?: { consulta: (parametrosConsulta?: {
//Todo: Esse filtros são recebido do processamento de filtro avandado
filtros?: tipoFiltro[]
coluna_ordem?: keyof T; coluna_ordem?: keyof T;
direcao_ordem?: "asc" | "desc"; direcao_ordem?: "asc" | "desc";
offSet?: number; offSet?: number;
@ -125,5 +126,18 @@ export type EliTabelaConsulta<T> = {
/** Função executada ao clicar no botão. */ /** Função executada ao clicar no botão. */
acao: () => void; acao: () => void;
}[]; }[];
/** configuração para aplicação dos filtros padrões */
// Todo: quando exite aparace ap lado do obtão coluna o potão filtro avançado, onde abre um modal com dua colunas de compoentes que são contruidas conforme esse padrão
// todo: Os filtros criados deverão ser salvo em local storagem como um objeto tipofiltro[]
filtroAvancado?: {
rotulo: string,
coluna: keyof T,
operador: operadores | keyof typeof operadores,
entrada: ComponenteEntrada
}[]
}; };

View file

@ -1,304 +0,0 @@
<template>
<div class="eli-input">
<!-- TEXT LIKE INPUTS -->
<v-text-field
v-if="isTextLike"
v-model="value"
:type="inputHtmlType"
:label="label"
:placeholder="placeholder"
:disabled="disabled"
:clearable="clearable && type !== 'password'"
:error="error"
:error-messages="errorMessages"
:hint="hint"
:persistent-hint="persistentHint"
:density="density"
:variant="variant"
:color="internalColor"
:inputmode="inputMode"
:suffix="type === 'porcentagem' ? '%' : undefined"
v-bind="attrs"
@focus="onFocus"
@blur="onBlur"
@input="onInput"
>
<!-- PASSWORD TOGGLE -->
<template
v-if="type === 'password' && showPasswordToggle"
#append-inner
>
<v-icon
class="cursor-pointer"
@click="togglePassword"
>
{{ showPassword ? "mdi-eye-off" : "mdi-eye" }}
</v-icon>
</template>
</v-text-field>
<!-- TEXTAREA -->
<v-textarea
v-else-if="type === 'textarea'"
v-model="value"
:label="label"
:rows="rows"
:density="density"
:variant="variant"
v-bind="attrs"
/>
<!-- SELECT -->
<v-select
v-else-if="type === 'select'"
v-model="value"
:items="computedItems"
:label="label"
:placeholder="placeholder"
:multiple="multiple"
:chips="chips"
:clearable="clearable"
:disabled="disabled"
:density="density"
:variant="variant"
item-title="label"
item-value="value"
:error="error"
:error-messages="errorMessages"
v-bind="attrs"
@focus="onFocus"
@blur="onBlur"
/>
<!-- RADIO -->
<v-radio-group
v-else-if="type === 'radio'"
v-model="value"
:row="row"
>
<v-radio
v-for="opt in computedItems"
:key="String(opt.value)"
:label="opt.label"
:value="opt.value"
/>
</v-radio-group>
<!-- CHECKBOX -->
<div v-else-if="type === 'checkbox'" class="checkbox-group">
<v-checkbox
v-for="opt in computedItems"
:key="String(opt.value)"
v-model="value"
:label="opt.label"
:value="opt.value"
:density="density"
/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed, PropType } from "vue";
import type {
CampoDensidade,
CampoOpcao,
CampoOpcaoBruta,
CampoTipo,
CampoValor,
CampoValorMultiplo,
CampoVariante,
} from "../../tipos";
import { formatarCpfCnpj } from "./utils/cpfCnpj";
import { formatTelefone } from "./utils/telefone";
import { formatarDecimal, formatarMoeda, formatarPorcentagem, somenteNumeros } from "./utils/numerico"
import { formatarCep } from "./utils/cep";
export default defineComponent({
name: "EliInput",
inheritAttrs: false,
props: {
/**
* Aceita valor simples (text-like) ou lista de valores (checkbox/select multiple).
* O componente não converte tipos automaticamente: mantém o que receber.
*/
modelValue: {
type: [String, Number, Boolean, Array] as PropType<CampoValor | CampoValorMultiplo>,
default: "",
},
type: { type: String as PropType<CampoTipo>, default: "text" },
label: String,
placeholder: String,
disabled: Boolean,
error: Boolean,
errorMessages: {
type: [String, Array] as PropType<string | string[]>,
default: () => [],
},
hint: String,
persistentHint: Boolean,
rows: { type: Number, default: 4 },
/**
* Para select/radio/checkbox.
* Aceita lista normalizada ({ label, value }) ou valores primitivos.
*/
options: {
type: Array as PropType<Array<CampoOpcaoBruta>>,
default: () => [],
},
clearable: Boolean,
variant: { type: String as PropType<CampoVariante>, default: "outlined" },
density: { type: String as PropType<CampoDensidade>, default: "comfortable" },
color: { type: String, default: "primary" },
row: Boolean,
showPasswordToggle: Boolean,
multiple: Boolean,
chips: Boolean,
},
emits: ["update:modelValue", "change", "focus", "blur"],
setup(props, { emit, attrs }) {
const focused = ref(false);
const showPassword = ref(false);
const value = computed({
get: () => props.modelValue,
set: (v: CampoValor | CampoValorMultiplo) => {
emit("update:modelValue", v);
emit("change", v);
},
});
const isTextLike = computed(() =>
[
"text",
"password",
"email",
"search",
"url",
"telefone",
"cpfCnpj",
"numericoInteiro",
"numericoDecimal",
"numericoMoeda",
"porcentagem",
"cep",
].includes(props.type)
);
const inputHtmlType = computed(() =>
props.type === "password"
? showPassword.value
? "text"
: "password"
: "text"
);
const inputMode = computed(() => {
if (props.type === "telefone") return "tel";
if (props.type === "porcentagem") return "decimal";
if (props.type.startsWith("numerico")) return "numeric";
return undefined;
});
const internalColor = computed(() =>
props.error ? "error" : focused.value ? props.color : undefined
);
function onInput(e: Event) {
const target = e.target as HTMLInputElement;
let resultado = target.value;
switch (props.type) {
case "numericoInteiro":
resultado = somenteNumeros(resultado);
break;
case "numericoDecimal":
resultado = formatarDecimal(resultado);
break;
case "numericoMoeda":
resultado = formatarMoeda(resultado);
break;
case "porcentagem":
resultado = formatarPorcentagem(resultado);
break;
case "telefone":
resultado = formatTelefone(resultado);
break;
case "cpfCnpj":
resultado = formatarCpfCnpj(resultado);
break;
case "cep":
resultado = formatarCep(resultado);
break;
}
target.value = resultado;
emit("update:modelValue", resultado);
emit("change", resultado);
}
function togglePassword() {
showPassword.value = !showPassword.value;
}
// --- Helpers para select / radio / checkbox (aceita objetos ou primitivos) ---
const computedItems = computed<Array<CampoOpcao>>(() => {
// Normaliza options para [{ label, value, disabled? }]
return (props.options || []).map((o) => {
if (o && typeof o === "object" && "value" in o) {
const valor = o.value as CampoValor;
return {
label: o.label ?? String(valor),
value: valor,
disabled: o.disabled,
};
}
const valor = o as CampoValor;
return { label: String(valor), value: valor };
});
});
return {
attrs,
value,
isTextLike,
inputHtmlType,
inputMode,
internalColor,
showPassword,
togglePassword,
onInput,
onFocus: () => emit("focus"),
onBlur: () => emit("blur"),
computedItems,
};
},
});
</script>
<style scoped>
.eli-input {
width: 100%;
}
.checkbox-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.cursor-pointer {
cursor: pointer;
}
</style>

View file

@ -1,118 +0,0 @@
# EliInput
**Componente base de input do design system**
O EliInput unifica vários tipos de campo (v-text-field, v-textarea, v-select, v-radio-group, v-checkbox) em uma única API consistente. Ele encapsula comportamentos, máscaras e regras comuns (CPF/CNPJ, telefone, CEP, numéricos, formatação de moeda etc.) para manter coerência visual e de lógica em toda a aplicação.
> ⚠️ Nunca use os componentes Vuetify diretamente fora do design system para esses casos.
> Utilize sempre EliInput para garantir formatação, tipagem e repasse de atributos padronizados.
---
## Visão geral
EliInput foi projetado para:
* Centralizar formatação (máscaras) para tipos de entrada comuns (telefone, CPF/CNPJ, CEP, numéricos).
* Fornecer uma API única (type) que representa diferentes controles (text, textarea, select, radio, checkbox, etc.).
* Repassar atributos/props do pai para o componente Vuetify interno (v-bind="$attrs") mantendo inheritAttrs: false.
* Emitir eventos padronizados: update:modelValue, change, focus, blur.
---
## Principais decisões de implementação
* inheritAttrs: false — o componente controla explicitamente para onde os atributos são passados (v-bind="attrs" no elemento interno).
* Uso de um computed value que faz emit("update:modelValue", v) e emit("change", v) — assim qualquer v-model no pai funciona como esperado.
* Normalização de props.options para aceitar objetos { label, value, disabled } ou primitivos ('A', 1).
* Separação clara entre lógica de formatação (aplicada em onInput) e componentes que não devem ser formatados (ex.: v-select).
---
## Tipagem (TypeScript)
```ts
type ValorCampo = string | number | boolean | null;
type Option = { label: string; value: ValorCampo; disabled?: boolean };
type InputVariant = 'outlined' | 'filled' | 'plain' | 'solo' | 'solo-filled' | 'solo-inverted' | 'underlined';
type Density = 'default' | 'comfortable' | 'compact';
type TipoNumerico = 'numericoInteiro' | 'numericoDecimal' | 'numericoMoeda' | 'porcentagem';
type InputType =
| 'text' | 'password' | 'email' | 'search' | 'url' | 'textarea'
| 'radio' | 'checkbox' | 'telefone' | 'cpfCnpj' | 'cep' | 'select'
| TipoNumerico;
```
---
## Props
| Prop | Tipo | Default | Descrição |
| ---------------- | --------------------------- | --------------- | ------------------------------------------------------ |
| `modelValue` | `string \| number \| boolean \| (string \| number \| boolean \| null)[]` | `""` | Valor controlado (use com `v-model`). |
| `type` | `InputType` | `"text"` | Tipo do controle (ver `InputType`). |
| `label` | `string` | `-` | Rótulo do campo. |
| `placeholder` | `string` | `-` | Texto exibido quando o campo está vazio. |
| `disabled` | `boolean` | `false` | Desabilita o campo. |
| `error` | `boolean` | `false` | Força estado visual de erro. |
| `errorMessages` | `string \| string[]` | `[]` | Mensagem ou lista de mensagens de erro. |
| `hint` | `string` | `-` | Texto de ajuda exibido abaixo do campo. |
| `persistentHint` | `boolean` | `false` | Mantém o hint sempre visível. |
| `variant` | `InputVariant` | `"outlined"` | Variante visual do Vuetify. |
| `density` | `Density` | `"comfortable"` | Densidade visual do campo. |
| `color` | `string` | `"primary"` | Cor do campo quando focado (ou `error`, se aplicável). |
| `clearable` | `boolean` | `false` | Permite limpar o valor do campo. |
## Notas sobre props
* options: aceita arrays como ['Frontend','Backend'] ou { label:'São Paulo', value:'SP' }. O componente normaliza para { label, value, disabled? }.
* type determina quais comportamentos internos são aplicados. Tipos numéricos e máscara (telefone, cpfCnpj, cep) passam por formatação em onInput.
* multiple/chips: úteis para type="select". O v-select interno recebe item-title="label" e item-value="value" para compatibilidade com objetos.
---
## Emissões (events)
* update:modelValue — padrão v-model.
* change — emitido sempre que o valor muda (alinha com update:modelValue).
* focus — quando o campo interno recebe foco.
* blur — quando perde o foco.
* Observação: como value é um computed com getter/setter que emite ambos, v-model e listeners de mudança no pai funcionarão normalmente.
---
## Repasso de atributos e listeners
* O componente define inheritAttrs: false e usa v-bind="attrs" nos componentes internos (v-text-field, v-select, etc.). Isso implica que:
* Atributos HTML (ex.: type, aria-label, class, style) passados para <EliInput> são aplicados ao componente Vuetify interno apropriado.
* Listeners (ex.: @click, @keydown) também fazem parte de $attrs e serão repassados para o componente interno — use o EliInput como se estivesse ouvindo eventos diretamente no input.
## Exemplo:
```vue
<EliInput type="text" v-model="nome" aria-label="Nome completo" @keydown.enter="enviar" />
```
---
## Comportamentos de formatação importantes
* numericoInteiro — remove tudo que não for dígito.
* numericoDecimal — mantém separador decimal (aplica formatarDecimal).
* numericoMoeda — formata para moeda conforme util (formatarMoeda).
* porcentagem — aplica formatação decimal e exibe sufixo `%` automaticamente.
* telefone — aplica máscara/format formatTelefone.
* cpfCnpj — aplica formatarCpfCnpj.
* cep — aplica formatarCep.
**Importante: a formatação ocorre no onInput (campos text-like). O v-select não passa por onInput — ele usa v-model="value" e o computed que emite o update. Se desejar formatação específica para itens do select (por exemplo, mostrar label formatado), trate nos options antes de passar.**
---
## Slot
* O componente não expõe slots customizados diretamente. Ele controla internamente o append para toggle de senha quando type === 'password' && showPasswordToggle (ícone de olho).
* Se você precisa de slots específicos do v-text-field/v-select, considere estender o componente ou criar uma variação que exponha os slots desejados.

View file

@ -1 +0,0 @@
export { default as EliInput } from "./EliInput.vue";

View file

@ -1,9 +0,0 @@
import { somenteNumeros } from "./numerico";
export function formatarCep(v: string): string {
const d = somenteNumeros(v).slice(0, 8);
if (d.length <= 5) return d;
return d.replace(/^(\d{5})(\d{1,3})$/, "$1-$2");
}

View file

@ -1,26 +0,0 @@
export function somenteNumeros(valor: string) {
return valor.replace(/\D+/g, "");
}
export function formatarDecimal(valor: string) {
const limpo = valor.replace(/[^\d,]/g, "");
const partes = limpo.split(",");
return partes.length > 2 ? partes[0] + "," + partes.slice(1).join("") : limpo;
}
/**
* Formatação para percentual:
* - remove '%' caso venha junto (ex: colar "10%")
* - mantém apenas dígitos e vírgula (no máximo uma)
*/
export function formatarPorcentagem(valor: string) {
return formatarDecimal(valor.replace(/%/g, ""));
}
export function formatarMoeda(valor: string) {
const numero = somenteNumeros(valor);
if (!numero) return "";
const inteiro = (parseInt(numero, 10) / 100).toFixed(2);
return inteiro.replace(".", ",").replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}

View file

@ -1,231 +0,0 @@
<template>
<div class="eli-data-hora">
<!--
Implementação propositalmente simples e estável:
- Usa o input nativo `datetime-local` (ou `date`) dentro do v-text-field.
- Evita depender de componentes experimentais (labs) do Vuetify.
- Recebe ISO 8601 (UTC/offset) e emite ISO 8601 com offset local.
Observação importante:
- `datetime-local` NÃO armazena timezone.
- Este componente converte a entrada para local para exibir ao usuário.
-->
<v-text-field
v-model="valor"
:type="tipoInput"
:label="rotulo"
:placeholder="placeholder"
:disabled="desabilitado"
:clearable="limpavel"
:error="erro"
:error-messages="mensagensErro"
:hint="dica"
:persistent-hint="dicaPersistente"
:density="densidade"
:variant="variante"
:min="minLocal"
:max="maxLocal"
v-bind="attrs"
@focus="emit('foco')"
@blur="emit('desfoco')"
/>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from "vue";
import dayjs from "dayjs";
import type { CampoDensidade, CampoVariante } from "../../tipos";
/**
* EliDataHora
*
* Campo para entrada de data + hora.
*
* Modelo:
* - O componente **recebe** `modelValue` em ISO 8601 (UTC `Z` ou com offset)
* - Converte para horário local para exibir (`date` ou `datetime-local`)
* - Ao editar, **emite** ISO 8601 com o **offset local**
*/
export default defineComponent({
name: "EliDataHora",
inheritAttrs: false,
props: {
/**
* Valor em ISO 8601:
* - com offset (ex.: `2026-01-09T13:15:00-03:00`)
* - ou UTC absoluto (ex.: `2026-01-09T16:15:00Z`)
*/
modelValue: {
type: String as PropType<string | null>,
default: null,
},
/**
* Define o tipo de entrada.
* - `dataHora`: usa `datetime-local`
* - `data`: usa `date`
*/
modo: {
type: String as PropType<"data" | "dataHora">,
default: "dataHora",
},
/** Rótulo exibido no v-text-field (Vuetify). */
rotulo: {
type: String,
default: "Data e hora",
},
/** Placeholder do input. */
placeholder: {
type: String,
default: "",
},
/** Desabilita a interação. */
desabilitado: {
type: Boolean,
default: false,
},
/** Se true, mostra ícone para limpar o valor (Vuetify clearable). */
limpavel: {
type: Boolean,
default: false,
},
/** Estado de erro (visual). */
erro: {
type: Boolean,
default: false,
},
/** Mensagens de erro. */
mensagensErro: {
type: [String, Array] as PropType<string | string[]>,
default: () => [],
},
/** Texto de apoio. */
dica: {
type: String,
default: "",
},
/** Mantém a dica sempre visível. */
dicaPersistente: {
type: Boolean,
default: false,
},
/** Densidade do campo (Vuetify). */
densidade: {
type: String as PropType<CampoDensidade>,
default: "comfortable",
},
/** Variante do v-text-field (Vuetify). */
variante: {
type: String as PropType<CampoVariante>,
default: "outlined",
},
/**
* Valor mínimo permitido.
* ISO 8601 (offset ou `Z`).
*/
min: {
// ISO 8601 (offset ou Z)
type: String as PropType<string | undefined>,
default: undefined,
},
/**
* Valor máximo permitido.
* ISO 8601 (offset ou `Z`).
*/
max: {
// ISO 8601 (offset ou Z)
type: String as PropType<string | undefined>,
default: undefined,
},
},
emits: {
/** v-model padrão. */
"update:modelValue": (_valor: string | null) => true,
/** Alias para consumidores que querem um evento semântico. */
alterar: (_valor: string | null) => true,
foco: () => true,
desfoco: () => true,
},
setup(props, { emit, attrs }) {
const tipoInput = computed<"date" | "datetime-local">(() =>
props.modo === "data" ? "date" : "datetime-local"
);
// Converte ISO (Z/offset) para o formato que o `datetime-local` aceita.
function isoParaInputDatetime(valorIso: string): string {
// `dayjs(valorIso)` interpreta ISO com timezone e converte para o local do usuário.
if (props.modo === "data") {
return dayjs(valorIso).format("YYYY-MM-DD");
}
return dayjs(valorIso).format("YYYY-MM-DDTHH:mm");
}
// Converte o valor do input (`YYYY-MM-DDTHH:mm`) para ISO 8601 com offset local.
function inputDatetimeParaIsoLocal(valorInput: string): string {
// `format()` retorna ISO 8601 com offset local.
// Em modo `data`, normalizamos para o começo do dia (00:00:00) no fuso local.
if (props.modo === "data") {
return dayjs(`${valorInput}T00:00`).format();
}
return dayjs(valorInput).format();
}
const valor = computed<string>({
get: () => {
if (!props.modelValue) return "";
return isoParaInputDatetime(props.modelValue);
},
set: (v) => {
// O `datetime-local` entrega string ou "" quando limpo.
const normalizado = v && v.length > 0 ? v : null;
if (!normalizado) {
emit("update:modelValue", null);
emit("alterar", null);
return;
}
const valorEmitido = inputDatetimeParaIsoLocal(normalizado);
emit("update:modelValue", valorEmitido);
emit("alterar", valorEmitido);
},
});
const minLocal = computed<string | undefined>(() => {
if (!props.min) return undefined;
return isoParaInputDatetime(props.min);
});
const maxLocal = computed<string | undefined>(() => {
if (!props.max) return undefined;
return isoParaInputDatetime(props.max);
});
return { attrs, valor, emit, minLocal, maxLocal, tipoInput };
},
});
</script>
<style scoped>
.eli-data-hora {
width: 100%;
}
</style>

View file

@ -1,108 +0,0 @@
# EliDataHora
O `EliDataHora` é um componente de **entrada de data e hora** baseado em `v-text-field` (Vuetify), usando o tipo nativo do HTML `datetime-local`.
Ele foi criado para oferecer uma solução **estável e leve** sem depender de componentes experimentais do Vuetify.
## Objetivo
- Permitir o usuário selecionar **data + hora** com UX nativa do navegador.
- Padronizar a API em português (props/eventos) no Design System.
## API
### Props
| Prop | Tipo | Padrão | Descrição |
|------|------|--------|-----------|
| `modelValue` | `string \| null` | `null` | **Sempre em ISO 8601**, aceitando UTC absoluto (`Z`) ou com offset (ex.: `2026-01-09T16:15:00Z`, `2026-01-09T13:15:00-03:00`). O componente converte para horário **local** antes de exibir. |
| `modo` | `"data" \| "dataHora"` | `"dataHora"` | Define se o campo permite selecionar apenas data (`date`) ou data+hora (`datetime-local`). |
| `rotulo` | `string` | `"Data e hora"` | Label do campo. |
| `placeholder` | `string` | `""` | Placeholder do campo. |
| `desabilitado` | `boolean` | `false` | Desabilita o campo. |
| `limpavel` | `boolean` | `false` | Habilita botão de limpar (Vuetify `clearable`). |
| `erro` | `boolean` | `false` | Estado de erro visual. |
| `mensagensErro` | `string \| string[]` | `[]` | Mensagens de erro. |
| `dica` | `string` | `""` | Hint/ajuda abaixo do campo. |
| `dicaPersistente` | `boolean` | `false` | Mantém dica sempre visível. |
| `densidade` | `CampoDensidade` | `"comfortable"` | Densidade (Vuetify). |
| `variante` | `CampoVariante` | `"outlined"` | Variante (Vuetify). |
| `min` | `string \| undefined` | `undefined` | Mínimo permitido em ISO 8601 (offset ou `Z`). |
| `max` | `string \| undefined` | `undefined` | Máximo permitido em ISO 8601 (offset ou `Z`). |
> Observação: o atributo HTML `datetime-local` **não inclui timezone**.
> Este componente resolve isso convertendo:
>
> - **entrada**: ISO 8601 (UTC/offset) → **exibição** em horário local
> - **saída**: valor selecionado → ISO 8601 com **offset local**
### Emits
| Evento | Payload | Quando dispara |
|--------|---------|---------------|
| `update:modelValue` | `string \| null` | Sempre que o valor muda (padrão do v-model). O payload é ISO 8601 com **offset local**. |
| `alterar` | `string \| null` | Alias semântico para mudanças de valor (mesmo payload do v-model). |
| `foco` | `void` | Ao focar o campo. |
| `desfoco` | `void` | Ao sair do foco. |
### Slots
Este componente não define slots próprios. Você pode usar slots do `v-text-field` via `v-bind="$attrs"` caso precise (ver exemplos abaixo).
## Exemplos
### 1) Uso básico com v-model
```vue
<template>
<EliDataHora v-model="dataHora" />
<div class="text-caption">Valor: {{ dataHora }}</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { EliDataHora } from "eli-vue";
export default defineComponent({
components: { EliDataHora },
setup() {
const dataHora = ref<string | null>("2026-01-09T16:15:00Z");
return { dataHora };
},
});
</script>
```
### 2) Com limites (min/max) e validação visual
```vue
<template>
<EliDataHora
v-model="dataHora"
rotulo="Agendar"
:min="min"
:max="max"
:erro="!dataHora"
:mensagensErro="!dataHora ? ['Obrigatório'] : []"
limpavel
/>
</template>
```
## Casos de borda / comportamento esperado
- Ao limpar o campo, o componente emite `null` (não string vazia).
- O navegador pode variar a UI do seletor (isso é esperado do `datetime-local`).
- `min/max` devem ser strings em ISO 8601 (offset ou `Z`).
- Em `modo="data"`, o componente emite ISO no **início do dia** (`00:00:00`) no fuso local.
## Acessibilidade
- O `v-text-field` do Vuetify já oferece base de acessibilidade.
- Sempre prefira passar `rotulo` significativo.
## Decisões de implementação
- Usamos `datetime-local` por ser amplamente suportado e não depender de APIs experimentais.
- O componente usa `dayjs` para converter entradas UTC/offset para local antes de exibir e para emitir ISO 8601 com offset local.
- Mantemos o valor como `string | null` para evitar conversões implícitas e permitir que cada projeto decida como persistir (UTC/local).

View file

@ -1 +0,0 @@
export { default as EliDataHora } from "./EliDataHora.vue";

View file

@ -11,100 +11,31 @@
<div class="grid-example"> <div class="grid-example">
<!-- text normal --> <!-- text normal -->
<EliInput
v-model="nome" <EliEntradaTexto
label="Nome" v-model:value="nome"
placeholder="Digite o nome" :opcoes="{ rotulo: 'Nome', placeholder: 'Digite o nome' }"
density="compact" density="compact"
/> />
<EliInput <EliEntradaTexto
v-model="idade" v-model:value="telefone"
type="numericoInteiro" :opcoes="{ rotulo: 'Telefone', formato: 'telefone' }"
label="Idade"
density="default"
/> />
<EliInput <EliEntradaTexto
v-model="altura" v-model:value="cep"
type="numericoDecimal" :opcoes="{ rotulo: 'CEP', placeholder: '00000-000', formato: 'cep' }"
label="Altura"
density="comfortable"
/> />
<EliInput v-model="valor" type="numericoMoeda" label="Valor" /> <EliEntradaTexto
v-model:value="documento"
<EliInput v-model="telefone" type="telefone" label="Telefone" /> :opcoes="{ rotulo: 'CPF / CNPJ', formato: 'cpfCnpj' }"
<EliInput
v-model="cep"
type="cep"
label="CEP"
placeholder="00000-000"
/> />
<EliInput <EliEntradaTexto
type="select" v-model:value="email"
label="Estado" :opcoes="{ rotulo: 'Email', placeholder: 'email@exemplo.com', formato: 'email' }"
:options="[
{ label: 'São Paulo', value: 'SP' },
{ label: 'Rio de Janeiro', value: 'RJ' }
]"
v-model="estado"
multiple
/>
<EliInput v-model="documento" type="cpfCnpj" label="CPF / CNPJ" />
<EliInput
v-model="email"
label="Email"
placeholder="email@exemplo.com"
/>
<EliInput
v-model="senha"
label="Senha"
type="password"
:showPasswordToggle="true"
placeholder="Digite sua senha"
/>
<!-- textarea -->
<EliInput
type="textarea"
v-model="mensagem"
label="Mensagem"
:rows="5"
/>
<!-- radio -->
<EliInput
type="radio"
v-model="cor"
label="Cor favorita"
:options="[
{ label: 'Azul', value: 'azul' },
{ label: 'Verde', value: 'verde' },
]"
/>
<!-- checkbox group -->
<EliInput
type="checkbox"
v-model="habilidades"
:options="[
{ label: 'Vue', value: 'vue' },
{ label: 'React', value: 'react' },
]"
/>
<!-- erro -->
<EliInput
v-model="nome"
label="Nome"
:error="true"
:error-messages="['Obrigatório']"
/> />
</div> </div>
</v-card-text> </v-card-text>
@ -121,45 +52,27 @@
import { defineComponent, ref } from "vue"; import { defineComponent, ref } from "vue";
import EliBotao from "../botao/EliBotao.vue"; import EliBotao from "../botao/EliBotao.vue";
import EliBadge from "../indicador/EliBadge.vue"; import EliBadge from "../indicador/EliBadge.vue";
import EliInput from "../campo/EliInput.vue"; import EliEntradaTexto from "../EliEntrada/EliEntradaTexto.vue";
type Habilidade = "vue" | "react";
export default defineComponent({ export default defineComponent({
name: "EliOlaMundo", name: "EliOlaMundo",
components: { components: {
EliBotao, EliBotao,
EliBadge, EliBadge,
EliInput, EliEntradaTexto,
}, },
setup() { setup() {
const nome = ref(""); const nome = ref("");
const estado = ref<string[]>([]);
const cep = ref(""); const cep = ref("");
const telefone = ref(""); const telefone = ref("");
const idade = ref("");
const altura = ref("");
const valor = ref("");
const email = ref(""); const email = ref("");
const mensagem = ref("");
const senha = ref("");
const documento = ref(""); const documento = ref("");
const cor = ref<"azul" | "verde" | null>(null);
const habilidades = ref<Habilidade[]>([]);
return { return {
nome, nome,
email, email,
documento, documento,
estado,
telefone, telefone,
mensagem,
senha,
cor,
habilidades,
idade,
altura,
cep, cep,
valor,
}; };
}, },
}); });

View file

@ -2,14 +2,14 @@
O `EliOlaMundo` é um componente **de exemplo** usado para validar rapidamente o Design System no playground. O `EliOlaMundo` é um componente **de exemplo** usado para validar rapidamente o Design System no playground.
> Ele não é um componente “de produto”; ele existe para demonstrar integração com Vuetify e mostrar variações de uso de `EliInput`, `EliBotao` e `EliBadge`. > Ele não é um componente “de produto”; ele existe para demonstrar integração com Vuetify e mostrar variações de uso de `EliEntradaTexto`, `EliEntradaNumero`, `EliBotao` e `EliBadge`.
## Estrutura do repositório (padrão) ## Estrutura do repositório (padrão)
Neste Design System: Neste Design System:
- **Pastas e arquivos** (quando aplicável) preferem português: `botao/`, `campo/`, `indicador/`, etc. - **Pastas e arquivos** (quando aplicável) preferem português: `botao/`, `EliEntrada/`, `indicador/`, etc.
- **Componentes** mantêm prefixo técnico `Eli` (PascalCase): `EliBotao`, `EliInput`. - **Componentes** mantêm prefixo técnico `Eli` (PascalCase): `EliBotao`, `EliEntradaTexto`.
Exemplo: Exemplo:
@ -19,9 +19,10 @@ src/componentes/
EliBotao.vue EliBotao.vue
index.ts index.ts
README.md README.md
campo/ EliEntrada/
EliInput.vue EliEntradaTexto.vue
index.ts EliEntradaNumero.vue
EliEntradaDataHora.vue
README.md README.md
``` ```

View file

@ -1,30 +0,0 @@
<template>
{{ String(dados?.numero).replace('.', ',') }}
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue"
import type { TiposTabelaCelulas } from "./tiposTabelaCelulas";
export default defineComponent({
name: "EliTabelaCelulaNumero",
components: {},
props: {
dados: {
type: Object as PropType<TiposTabelaCelulas["numero"]>,
},
},
data() {
return {
}
},
methods: {
},
setup({ dados }) {
return { dados }
},
})
</script>
<style scoped></style>

View file

@ -1,30 +0,0 @@
<template>
{{ dados?.texto }}
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue"
import type { TiposTabelaCelulas } from "./tiposTabelaCelulas";
export default defineComponent({
name: "EliTabelaCelulaTextoSimples",
components: {},
props: {
dados: {
type: Object as PropType<TiposTabelaCelulas["textoSimples"]>,
},
},
data() {
return {
}
},
methods: {
},
setup({ dados }) {
return { dados }
},
})
</script>
<style scoped></style>

View file

@ -3,28 +3,27 @@ import "./styles/eli-vue-fonts.css";
import { EliOlaMundo } from "./componentes/ola_mundo"; import { EliOlaMundo } from "./componentes/ola_mundo";
import { EliBotao } from "./componentes/botao"; import { EliBotao } from "./componentes/botao";
import { EliBadge } from "./componentes/indicador"; import { EliBadge } from "./componentes/indicador";
import { EliInput } from "./componentes/campo";
import { EliCartao } from "./componentes/cartao"; import { EliCartao } from "./componentes/cartao";
import { EliDataHora } from "./componentes/data_hora"; import { EliTabela } from "./componentes/EliTabela";
import { EliTabela } from "./components/eli/EliTabela"; import { EliEntradaTexto, EliEntradaNumero, EliEntradaDataHora } from "./componentes/EliEntrada";
export { EliOlaMundo }; export { EliOlaMundo };
export { EliBotao }; export { EliBotao };
export { EliBadge }; export { EliBadge };
export { EliInput };
export { EliCartao }; export { EliCartao };
export { EliDataHora };
export { EliTabela }; export { EliTabela };
export { EliEntradaTexto, EliEntradaNumero, EliEntradaDataHora };
const EliVue: Plugin = { const EliVue: Plugin = {
install(app: App) { install(app: App) {
app.component("EliOlaMundo", EliOlaMundo); app.component("EliOlaMundo", EliOlaMundo);
app.component("EliBotao", EliBotao); app.component("EliBotao", EliBotao);
app.component("EliBadge", EliBadge); app.component("EliBadge", EliBadge);
app.component("EliInput", EliInput);
app.component("EliCartao", EliCartao); app.component("EliCartao", EliCartao);
app.component("EliDataHora", EliDataHora);
app.component("EliTabela", EliTabela); app.component("EliTabela", EliTabela);
app.component("EliEntradaTexto", EliEntradaTexto);
app.component("EliEntradaNumero", EliEntradaNumero);
app.component("EliEntradaDataHora", EliEntradaDataHora);
}, },
}; };

View file

@ -8,7 +8,7 @@
<v-tab value="botao">Botão</v-tab> <v-tab value="botao">Botão</v-tab>
<v-tab value="indicador">Indicador</v-tab> <v-tab value="indicador">Indicador</v-tab>
<v-tab value="cartao">Cartão</v-tab> <v-tab value="cartao">Cartão</v-tab>
<v-tab value="campo">Campo</v-tab> <v-tab value="entradas">Entradas</v-tab>
<v-tab value="data_hora">Data e hora</v-tab> <v-tab value="data_hora">Data e hora</v-tab>
<v-tab value="tabela">Tabela</v-tab> <v-tab value="tabela">Tabela</v-tab>
<v-tab value="ola_mundo">Demo</v-tab> <v-tab value="ola_mundo">Demo</v-tab>
@ -19,7 +19,7 @@
<BotaoPlayground v-if="aba === 'botao'" /> <BotaoPlayground v-if="aba === 'botao'" />
<IndicadorPlayground v-else-if="aba === 'indicador'" /> <IndicadorPlayground v-else-if="aba === 'indicador'" />
<CartaoPlayground v-else-if="aba === 'cartao'" /> <CartaoPlayground v-else-if="aba === 'cartao'" />
<CampoPlayground v-else-if="aba === 'campo'" /> <EntradasPlayground v-else-if="aba === 'entradas'" />
<DataHoraPlayground v-else-if="aba === 'data_hora'" /> <DataHoraPlayground v-else-if="aba === 'data_hora'" />
<TabelaPlayground v-else-if="aba === 'tabela'" /> <TabelaPlayground v-else-if="aba === 'tabela'" />
<OlaMundoPlayground v-else /> <OlaMundoPlayground v-else />
@ -31,7 +31,7 @@ import { defineComponent } from "vue";
import BotaoPlayground from "./botao.playground.vue"; import BotaoPlayground from "./botao.playground.vue";
import IndicadorPlayground from "./indicador.playground.vue"; import IndicadorPlayground from "./indicador.playground.vue";
import CartaoPlayground from "./cartao.playground.vue"; import CartaoPlayground from "./cartao.playground.vue";
import CampoPlayground from "./campo.playground.vue"; import EntradasPlayground from "./entradas.playground.vue";
import DataHoraPlayground from "./data_hora.playground.vue"; import DataHoraPlayground from "./data_hora.playground.vue";
import TabelaPlayground from "./tabela.playground.vue"; import TabelaPlayground from "./tabela.playground.vue";
import OlaMundoPlayground from "./ola_mundo.playground.vue"; import OlaMundoPlayground from "./ola_mundo.playground.vue";
@ -40,7 +40,7 @@ type AbaPlayground =
| "botao" | "botao"
| "indicador" | "indicador"
| "cartao" | "cartao"
| "campo" | "entradas"
| "data_hora" | "data_hora"
| "tabela" | "tabela"
| "ola_mundo"; | "ola_mundo";
@ -49,7 +49,7 @@ const mapaHashParaAba: Record<string, AbaPlayground> = {
botao: "botao", botao: "botao",
indicador: "indicador", indicador: "indicador",
cartao: "cartao", cartao: "cartao",
campo: "campo", entradas: "entradas",
"data-hora": "data_hora", "data-hora": "data_hora",
tabela: "tabela", tabela: "tabela",
demo: "ola_mundo", demo: "ola_mundo",
@ -59,7 +59,7 @@ const mapaAbaParaHash: Record<AbaPlayground, string> = {
botao: "botao", botao: "botao",
indicador: "indicador", indicador: "indicador",
cartao: "cartao", cartao: "cartao",
campo: "campo", entradas: "entradas",
data_hora: "data-hora", data_hora: "data-hora",
tabela: "tabela", tabela: "tabela",
ola_mundo: "demo", ola_mundo: "demo",
@ -71,7 +71,7 @@ export default defineComponent({
BotaoPlayground, BotaoPlayground,
IndicadorPlayground, IndicadorPlayground,
CartaoPlayground, CartaoPlayground,
CampoPlayground, EntradasPlayground,
DataHoraPlayground, DataHoraPlayground,
TabelaPlayground, TabelaPlayground,
OlaMundoPlayground, OlaMundoPlayground,

View file

@ -1,127 +0,0 @@
<template>
<section class="stack">
<h2>EliInput (campo)</h2>
<div class="grid">
<EliInput v-model="nome" label="Nome" placeholder="Digite seu nome" />
<EliInput v-model="telefone" type="telefone" label="Telefone" />
<EliInput v-model="documento" type="cpfCnpj" label="CPF / CNPJ" />
<EliInput v-model="idade" type="numericoInteiro" label="Idade" />
<EliInput v-model="valor" type="numericoMoeda" label="Valor" />
<EliInput v-model="taxa" type="porcentagem" label="Taxa" placeholder="0,00" />
<EliInput v-model="cep" type="cep" label="CEP" placeholder="00000-000" />
<EliInput
v-model="estado"
type="select"
label="Estado"
multiple
:options="[
{ label: 'São Paulo', value: 'SP' },
{ label: 'Rio de Janeiro', value: 'RJ' },
]"
/>
<EliInput
v-model="cor"
type="radio"
label="Cor favorita"
:options="[
{ label: 'Azul', value: 'azul' },
{ label: 'Verde', value: 'verde' },
]"
/>
<EliInput
v-model="habilidades"
type="checkbox"
label="Habilidades"
:options="[
{ label: 'Vue', value: 'vue' },
{ label: 'React', value: 'react' },
]"
/>
</div>
<pre class="debug">{{ debug }}</pre>
</section>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from "vue";
import { EliInput } from "@/componentes/campo";
type Cor = "azul" | "verde" | null;
type Habilidade = "vue" | "react";
export default defineComponent({
name: "CampoPlayground",
components: { EliInput },
setup() {
const nome = ref("");
const telefone = ref("");
const documento = ref("");
const idade = ref("");
const valor = ref("");
const taxa = ref("");
const cep = ref("");
const estado = ref<string[]>([]);
const cor = ref<Cor>(null);
const habilidades = ref<Habilidade[]>([]);
const debug = computed(() =>
JSON.stringify(
{
nome: nome.value,
telefone: telefone.value,
documento: documento.value,
idade: idade.value,
valor: valor.value,
taxa: taxa.value,
cep: cep.value,
estado: estado.value,
cor: cor.value,
habilidades: habilidades.value,
},
null,
2
)
);
return {
nome,
telefone,
documento,
idade,
valor,
taxa,
cep,
estado,
cor,
habilidades,
debug,
};
},
});
</script>
<style scoped>
.stack {
display: grid;
gap: 16px;
}
.grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.debug {
padding: 12px;
background: rgba(0, 0, 0, 0.04);
border-radius: 8px;
overflow: auto;
}
</style>

View file

@ -3,10 +3,9 @@
<h2>EliCartao (pipeline)</h2> <h2>EliCartao (pipeline)</h2>
<div class="toolbar"> <div class="toolbar">
<EliInput <EliEntradaTexto
v-model="filtro" v-model:value="filtro"
label="Buscar" :opcoes="{ rotulo: 'Buscar', placeholder: 'Cliente, título, valor...' }"
placeholder="Cliente, título, valor..."
density="compact" density="compact"
/> />
@ -48,7 +47,7 @@
import { defineComponent, ref } from "vue"; import { defineComponent, ref } from "vue";
import { EliBadge } from "@/componentes/indicador"; import { EliBadge } from "@/componentes/indicador";
import { EliBotao } from "@/componentes/botao"; import { EliBotao } from "@/componentes/botao";
import { EliInput } from "@/componentes/campo"; import { EliEntradaTexto } from "@/componentes/EliEntrada";
import { EliCartao } from "@/componentes/cartao"; import { EliCartao } from "@/componentes/cartao";
import type { CartaoStatus } from "@/tipos"; import type { CartaoStatus } from "@/tipos";
@ -68,7 +67,7 @@ type Coluna = {
export default defineComponent({ export default defineComponent({
name: "CartaoPlayground", name: "CartaoPlayground",
components: { EliBadge, EliBotao, EliInput, EliCartao }, components: { EliBadge, EliBotao, EliEntradaTexto, EliCartao },
setup() { setup() {
const filtro = ref(""); const filtro = ref("");

Some files were not shown because too many files have changed in this diff Show more