This commit is contained in:
Luiz Silva 2026-01-27 14:48:51 -03:00
parent df798df8d7
commit 50a971ccaf
17 changed files with 1516 additions and 619 deletions

2
dist/eli-vue.css vendored

File diff suppressed because one or more lines are too long

1689
dist/eli-vue.es.js vendored

File diff suppressed because it is too large Load diff

62
dist/eli-vue.umd.js vendored

File diff suppressed because one or more lines are too long

BIN
dist/quero-quero.gif vendored Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View file

@ -1,5 +1,5 @@
import { PropType } from "vue";
import type { EliTabelaConsulta } from "./types";
import type { EliTabelaConsulta } from "./types-eli-tabela";
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
tabela: {
type: PropType<EliTabelaConsulta<any>>;

View file

@ -0,0 +1,24 @@
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
modelo: {
type: StringConstructor;
required: false;
default: string;
};
}>, {
texto: import("vue").Ref<string, string>;
emitirBusca: () => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
buscar(valor: string): boolean;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
modelo: {
type: StringConstructor;
required: false;
default: string;
};
}>> & Readonly<{
onBuscar?: ((valor: string) => any) | undefined;
}>, {
modelo: string;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
declare const _default: typeof __VLS_export;
export default _default;

View file

@ -0,0 +1,45 @@
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
pagina: {
type: NumberConstructor;
required: true;
};
totalPaginas: {
type: NumberConstructor;
required: true;
};
maximoBotoes: {
type: NumberConstructor;
required: false;
};
}>, {
botoes: import("vue").ComputedRef<{
label: string;
pagina?: number;
ativo?: boolean;
ehEllipsis?: boolean;
}[]>;
irParaPagina: (pagina: number | undefined) => void;
anteriorDesabilitado: import("vue").ComputedRef<boolean>;
proximaDesabilitada: import("vue").ComputedRef<boolean>;
paginaAtual: import("vue").ComputedRef<number>;
totalPaginasExibidas: import("vue").ComputedRef<number>;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
alterar(pagina: number): boolean;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
pagina: {
type: NumberConstructor;
required: true;
};
totalPaginas: {
type: NumberConstructor;
required: true;
};
maximoBotoes: {
type: NumberConstructor;
required: false;
};
}>> & Readonly<{
onAlterar?: ((pagina: number) => any) | undefined;
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
declare const _default: typeof __VLS_export;
export default _default;

View file

@ -1,3 +1,3 @@
export { default as EliTabela } from "./EliTabela.vue";
export * from "./types";
export * from "./types-eli-tabela";
export * from "./celulas/EliCelulaTextoSimples";

View file

@ -0,0 +1,85 @@
import type { tipoResposta } from "p-respostas";
import type { LucideIcon } from "lucide-vue-next";
import type { VNodeChild } from "vue";
export type ComponenteCelula = VNodeChild;
export type EliColuna<T> = {
/** Texto exibido no cabeçalho da coluna. */
rotulo: string;
/** Função responsável por renderizar o conteúdo da célula. */
celula: (linha: T) => ComponenteCelula;
/** Ação opcional disparada ao clicar na célula. */
acao?: () => void;
/**
* 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.
*/
coluna_ordem?: keyof T;
};
export type EliConsultaPaginada<T> = {
/** Registros retornados na consulta. */
valores: T[];
/** Total de registros disponíveis no backend. */
quantidade: number;
};
export type EliTabelaAcao<T> = {
/** Ícone (Lucide) exibido para representar a ação. */
icone: LucideIcon;
/** Cor aplicada ao ícone e rótulo. */
cor: string;
/** Texto descritivo da ação. */
rotulo: string;
/** Função executada quando o usuário ativa a ação. */
acao: (linha: T) => void;
/**
* Define se a ação deve ser exibida para a linha. Pode ser um booleano fixo
* ou uma função (sincrona/assíncrona) que recebe a linha para decisão dinâmica.
*/
exibir?: boolean | ((linha: T) => Promise<boolean> | boolean);
};
/**
* Estrutura de dados para uma tabela alimentada por uma consulta.
*
* - `colunas`: definição de colunas e como renderizar cada célula
* - `consulta`: função que recupera os dados, com suporte a ordenação/paginação
* - `mostrarCaixaDeBusca`: habilita um campo de busca textual no cabeçalho
*/
export type EliTabelaConsulta<T> = {
/** Indica se a caixa de busca deve ser exibida acima da tabela. */
mostrarCaixaDeBusca?: boolean;
/** Lista de colunas da tabela. */
colunas: EliColuna<T>[];
/** Quantidade de registros solicitados por consulta (padrão `10`). */
registros_por_consulta?: number;
/**
* Função responsável por buscar os dados. Recebe parâmetros opcionais de
* ordenação (`coluna_ordem`/`direcao_ordem`) e paginação (`offSet`/`limit`).
*/
consulta: (parametrosConsulta?: {
coluna_ordem?: keyof T;
direcao_ordem?: "asc" | "desc";
offSet?: number;
limit?: number;
/** Texto digitado na caixa de busca, quando habilitada. */
texto_busca?: string;
}) => Promise<tipoResposta<EliConsultaPaginada<T>>>;
/** Quantidade máxima de botões exibidos na paginação (padrão `7`). */
maximo_botoes_paginacao?: number;
/** Mensagem exibida quando a consulta retorna ok porém sem dados. */
mensagemVazio?: string;
/** Ações exibidas à direita de cada linha. */
acoesLinha?: EliTabelaAcao<T>[];
/**
* Configurações dos botões que serão inseridos a direita da caixa de busca.
* Seu uso mais comum será para criar novos registros, mas poderá ter outras utilidades.
*/
acoesTabela?: {
/** Ícone (Lucide) exibido no botão */
icone?: LucideIcon;
/** Cor aplicada ao botão. */
cor?: string;
/** Texto descritivo da ação. */
rotulo: string;
/** Função executada ao clicar no botão. */
acao: () => void;
}[];
};

View file

@ -1,24 +0,0 @@
import type { tipoResposta } from "p-respostas";
import type { VNodeChild } from "vue";
export type ComponenteCelula = VNodeChild;
export type EliColuna<T> = {
rotulo: string;
celula: (linha: T) => ComponenteCelula;
acao?: () => void;
};
export type EliConsultaPaginada<T> = {
valores: T[];
quantidade: number;
};
/**
* Estrutura de dados para uma tabela alimentada por uma consulta.
*
* - `colunas`: definição de colunas e como renderizar cada célula
* - `resposta`: função assíncrona que retorna uma resposta padronizada
*/
export type EliTabelaConsulta<T> = {
colunas: EliColuna<T>[];
resposta: () => Promise<tipoResposta<EliConsultaPaginada<T>>>;
/** Mensagem exibida quando a consulta retorna ok porém sem dados. */
mensagemVazio?: string;
};

1
dist/types/constantes.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export declare const gif_quero_quero = "https://paiol.idz.one/estaticos/quero-quero.gif";

View file

@ -1,6 +1,6 @@
{
"name": "eli-vue",
"version": "0.1.22",
"version": "0.1.23",
"private": false,
"main": "./dist/eli-vue.umd.js",
"module": "./dist/eli-vue.es.js",

BIN
public/quero-quero.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View file

@ -37,6 +37,8 @@ export default defineComponent({
const colunaOrdenacao = ref<string | null>(null);
const direcaoOrdenacao = ref<"asc" | "desc">("asc");
const exibirBusca = computed(() => Boolean(props.tabela.mostrarCaixaDeBusca));
const acoesCabecalho = computed(() => props.tabela.acoesTabela ?? []);
const temAcoesCabecalho = computed(() => acoesCabecalho.value.length > 0);
const registrosPorConsulta = computed(() => {
const valor = props.tabela.registros_por_consulta;
if (typeof valor === "number" && valor > 0) {
@ -235,15 +237,15 @@ export default defineComponent({
return;
}
const acoes = tabelaConfig.acoesLinha ?? [];
const acoesLinhaConfiguradas = tabelaConfig.acoesLinha ?? [];
if (!acoes.length) {
if (!acoesLinhaConfiguradas.length) {
acoesVisiveis.value = [];
return;
}
const preResultado = valores.map(() =>
acoes.map((acao) => {
acoesLinhaConfiguradas.map((acao) => {
if (acao.exibir === undefined) {
return true;
}
@ -261,7 +263,7 @@ export default defineComponent({
const visibilidade = await Promise.all(
valores.map(async (linha) =>
Promise.all(
acoes.map(async (acao) => {
acoesLinhaConfiguradas.map(async (acao) => {
if (acao.exibir === undefined) {
return true;
}
@ -372,8 +374,8 @@ export default defineComponent({
}
const colunas = tabela.colunas;
const acoes = tabela.acoesLinha ?? [];
const temAcoes = acoes.length > 0;
const acoesLinha = tabela.acoesLinha ?? [];
const temAcoes = acoesLinha.length > 0;
if (!linhas.value.length) {
return renderVazio(tabela.mensagemVazio);
@ -445,12 +447,49 @@ export default defineComponent({
const conteudoTabela: ReturnType<typeof h>[] = [];
if (exibirBusca.value) {
if (exibirBusca.value || temAcoesCabecalho.value) {
const botoes = acoesCabecalho.value.map((botao, indice) =>
h(
"button",
{
key: `${botao.rotulo}-${indice}`,
type: "button",
class: "eli-tabela__acoes-cabecalho-botao",
// Quando `cor` for informada, tratamos como cor de destaque do botão.
// Também ajustamos o texto/ícone para branco para manter contraste.
style: botao.cor
? {
backgroundColor: botao.cor,
color: "#fff",
}
: undefined,
onClick: botao.acao,
},
[
botao.icone
? h(botao.icone, {
class: "eli-tabela__acoes-cabecalho-icone",
size: 16,
strokeWidth: 2,
})
: null,
h("span", { class: "eli-tabela__acoes-cabecalho-rotulo" }, botao.rotulo),
]
)
);
conteudoTabela.push(
h(EliTabelaCaixaDeBusca, {
modelo: valorBusca.value,
onBuscar: atualizarBusca,
})
h("div", { class: "eli-tabela__cabecalho" }, [
exibirBusca.value
? h(EliTabelaCaixaDeBusca, {
modelo: valorBusca.value,
onBuscar: atualizarBusca,
})
: null,
temAcoesCabecalho.value
? h("div", { class: "eli-tabela__acoes-cabecalho" }, botoes)
: null,
])
);
}
@ -486,7 +525,7 @@ export default defineComponent({
if (temAcoes) {
const visibilidade = acoesVisiveis.value[i] ?? [];
const acoesDisponiveis = acoes
const acoesDisponiveis = acoesLinha
.map((acao, indice) => {
const fallbackVisivel =
acao.exibir === undefined
@ -503,7 +542,7 @@ export default defineComponent({
})
.filter((item) => item.visivel);
const possuiAcoes = acoesDisponiveis.length > 0;
const possuiAcoes = acoesDisponiveis.length > 0;
if (!possuiAcoes && menuAberto.value === i) {
menuAberto.value = null;
@ -603,7 +642,7 @@ export default defineComponent({
classesContainer.push("eli-tabela__acoes-container--aberto");
}
celulas.push(
celulas.push(
h(
"td",
{
@ -796,6 +835,55 @@ export default defineComponent({
z-index: 200;
}
.eli-tabela__cabecalho {
display: flex;
align-items: center;
/* Ações ficam imediatamente à direita da busca (em vez de ir para a extrema direita) */
justify-content: flex-start;
gap: 12px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.eli-tabela__acoes-cabecalho {
display: inline-flex;
gap: 8px;
flex-wrap: wrap;
}
.eli-tabela__acoes-cabecalho-botao {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 9999px;
border: none;
background: rgba(37, 99, 235, 0.12);
color: rgba(37, 99, 235, 0.95);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
}
.eli-tabela__acoes-cabecalho-botao:hover,
.eli-tabela__acoes-cabecalho-botao:focus-visible {
background: rgba(37, 99, 235, 0.2);
}
.eli-tabela__acoes-cabecalho-botao:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.35);
outline-offset: 2px;
}
.eli-tabela__acoes-cabecalho-icone {
display: inline-block;
}
.eli-tabela__acoes-cabecalho-rotulo {
line-height: 1;
}
.eli-tabela__acoes-toggle {
display: inline-flex;
align-items: center;

View file

@ -41,6 +41,7 @@ export type EliTabelaAcao<T> = {
exibir?: boolean | ((linha: T) => Promise<boolean> | boolean);
};
/**
* Estrutura de dados para uma tabela alimentada por uma consulta.
*
@ -73,19 +74,19 @@ export type EliTabelaConsulta<T> = {
mensagemVazio?: string;
/** Ações exibidas à direita de cada linha. */
acoesLinha?: EliTabelaAcao<T>[];
/** configurações do botões que serão inseridos a direta da caixa de busca, seu uso mais como será para criar novo regitros, mas poderá ter outras utilidades */
acoesTabela?: {
/** Ícone (Lucide) exibido no botão */
icone?: LucideIcon;
/** Cor aplicada ao botão. */
cor?: string;
/** Texto descritivo da ação. */
rotulo: string;
/** Função executada ao clicar no botão. */
acao: () => void;
}[];
/**
* Configurações dos botões que serão inseridos a direita da caixa de busca.
* Seu uso mais comum será para criar novos registros, mas poderá ter outras utilidades.
*/
acoesTabela?: {
/** Ícone (Lucide) exibido no botão */
icone?: LucideIcon;
/** Cor aplicada ao botão. */
cor?: string;
/** Texto descritivo da ação. */
rotulo: string;
/** Função executada ao clicar no botão. */
acao: () => void;
}[];
};

1
src/constantes.ts Normal file
View file

@ -0,0 +1 @@
export const gif_quero_quero = 'https://paiol.idz.one/estaticos/quero-quero.gif'

View file

@ -2,7 +2,8 @@
<section class="stack">
<h2>EliTabela</h2>
<EliTabela :tabela="tabelaOk" />
<!-- Exemplo: botão de ação "Novo" no cabeçalho que adiciona uma linha -->
<EliTabela :tabela="tabelaOk" :key="versaoTabelaOk" />
<EliTabela :tabela="tabelaVazia" />
@ -11,9 +12,9 @@
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, ref } from "vue";
import { codigosResposta } from "p-respostas";
import { Eye, Trash2 } from "lucide-vue-next";
import { Eye, Plus, Trash2 } from "lucide-vue-next";
import { EliTabela } from "@/components/eli/EliTabela";
import type { EliTabelaConsulta } from "@/components/eli/EliTabela";
@ -29,7 +30,7 @@ export default defineComponent({
name: "TabelaPlayground",
components: { EliTabela },
setup() {
const acoesTabela: EliTabelaConsulta<Linha>["acoesLinha"] = [
const acoesLinha: EliTabelaConsulta<Linha>["acoesLinha"] = [
{
icone: Eye,
cor: "#2563eb",
@ -49,7 +50,7 @@ export default defineComponent({
},
];
const linhasPadrao: Linha[] = [
const linhasPadrao = ref<Linha[]>([
{
empreendedor: "Maria Silva",
empreendimento: "Doces da Maria",
@ -197,7 +198,25 @@ export default defineComponent({
email: "viviane@castroarte.com",
telefone: "(81) 98787-1212",
},
];
]);
// Incrementamos a chave para forçar o EliTabela a recarregar a consulta.
// (Como o componente não expõe um método público de refresh)
const versaoTabelaOk = ref(0);
function adicionarLinha() {
const proximo = linhasPadrao.value.length + 1;
linhasPadrao.value.unshift({
empreendedor: `Novo Empreendedor ${proximo}`,
empreendimento: `Novo Empreendimento ${proximo}`,
documento: "00.000.000/0000-00",
email: `novo${proximo}@exemplo.com`,
telefone: "(00) 90000-0000",
});
versaoTabelaOk.value++;
}
const filtrarPorBusca = (linhas: Linha[], texto?: string) => {
@ -256,6 +275,14 @@ export default defineComponent({
const tabelaOk: EliTabelaConsulta<Linha> = {
registros_por_consulta: 10,
mostrarCaixaDeBusca: true,
acoesTabela: [
{
icone: Plus,
cor: "#16a34a",
rotulo: "Novo",
acao: adicionarLinha,
},
],
colunas: [
{
rotulo: "Empreendedor",
@ -287,9 +314,9 @@ export default defineComponent({
coluna_ordem: "telefone",
},
],
acoesLinha: acoesTabela,
acoesLinha: acoesLinha,
consulta: async (parametrosConsulta) => {
const ordenadas = ordenarLinhas(linhasPadrao, parametrosConsulta);
const ordenadas = ordenarLinhas(linhasPadrao.value, parametrosConsulta);
const valores = aplicarPaginacao(ordenadas, parametrosConsulta);
return {
@ -322,14 +349,14 @@ export default defineComponent({
};
},
mensagemVazio: "Nada para mostrar aqui.",
acoesLinha: acoesTabela,
acoesLinha: acoesLinha,
};
const tabelaErro: EliTabelaConsulta<Linha> = {
registros_por_consulta: tabelaOk.registros_por_consulta,
mostrarCaixaDeBusca: tabelaOk.mostrarCaixaDeBusca,
colunas: tabelaOk.colunas,
acoesLinha: acoesTabela,
acoesLinha: acoesLinha,
consulta: async (_parametrosConsulta) => {
return {
cod: codigosResposta.erroConhecido,
@ -341,7 +368,7 @@ export default defineComponent({
},
};
return { tabelaOk, tabelaVazia, tabelaErro };
return { tabelaOk, tabelaVazia, tabelaErro, versaoTabelaOk };
},
});
</script>