This commit is contained in:
Luiz Silva 2026-01-27 15:51:54 -03:00
parent 50a971ccaf
commit 64535c51a3
12 changed files with 1016 additions and 774 deletions

2
.agent
View file

@ -23,7 +23,7 @@ Construir um Design System de componentes em **Vue 3** para reutilização em m
- **defineComponent** (obrigatório)
- Sem TSX (padrão: `<template>` + `<script lang="ts">`)
- Estilo: preferir CSS scoped por componente (se aplicável)
- Ícones: se usar, definir um padrão único do repositório (não inventar por componente)
- Ícones: caso seja necessário o uso de ícones, usar **lucide** (biblioteca `lucide-vue-next`) como padrão do repositório.
---

2
dist/eli-vue.css vendored

File diff suppressed because one or more lines are too long

1531
dist/eli-vue.es.js vendored

File diff suppressed because it is too large Load diff

29
dist/eli-vue.umd.js vendored

File diff suppressed because one or more lines are too long

View file

@ -19,6 +19,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
onBuscar?: ((valor: string) => any) | undefined;
}>, {
modelo: string;
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
}, {}, {
Search: import("vue").FunctionalComponent<import("lucide-vue-next").LucideProps, {}, any, {}>;
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
declare const _default: typeof __VLS_export;
export default _default;

View file

@ -2,6 +2,7 @@ import type { tipoResposta } from "p-respostas";
import type { LucideIcon } from "lucide-vue-next";
import type { VNodeChild } from "vue";
export type ComponenteCelula = VNodeChild;
export type EliAlinhamentoColuna = "esquerda" | "centro" | "direita";
export type EliColuna<T> = {
/** Texto exibido no cabeçalho da coluna. */
rotulo: string;
@ -9,6 +10,19 @@ export type EliColuna<T> = {
celula: (linha: T) => ComponenteCelula;
/** 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
* exibir controles de ordenação e utiliza o valor como chave para o backend.

View file

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

View file

@ -140,6 +140,53 @@ export default defineComponent({
return filhos as never;
}
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 renderConteudoCelula(valor: unknown, coluna: (typeof props.tabela.colunas)[number]) {
const filhos = normalizarFilhos(valor);
const truncar = Boolean(coluna.truncar);
if (!truncar) {
return filhos;
}
// Só truncamos de forma segura quando o conteúdo é textual.
if (typeof filhos !== "string" && typeof filhos !== "number") {
return filhos;
}
const tooltip = String(filhos);
return h(
"span",
{
class: "eli-tabela__celula-conteudo",
style: coluna.largura_maxima
? {
maxWidth: obterMaxWidth(coluna.largura_maxima),
}
: undefined,
title: tooltip,
},
tooltip
);
}
function renderErro(mensagem: string) {
return h(
"div",
@ -428,6 +475,7 @@ export default defineComponent({
class: [
"eli-tabela__th",
ordenavel ? "eli-tabela__th--ordenavel" : undefined,
obterClasseAlinhamento(coluna.alinhamento),
],
scope: "col",
},
@ -515,11 +563,12 @@ export default defineComponent({
class: [
"eli-tabela__td",
coluna.acao ? "eli-tabela__td--clicavel" : undefined,
obterClasseAlinhamento(coluna.alinhamento),
],
key: `${i}-${j}`,
onClick: coluna.acao ? () => coluna.acao?.() : undefined,
},
normalizarFilhos(coluna.celula(linha as never))
renderConteudoCelula(coluna.celula(linha as never), coluna)
)
);
@ -663,7 +712,10 @@ export default defineComponent({
return h(
"tr",
{ class: "eli-tabela__tr", key: i },
{
class: ["eli-tabela__tr", i % 2 === 1 ? "eli-tabela__tr--zebra" : undefined],
key: i,
},
celulas
);
})
@ -710,6 +762,10 @@ export default defineComponent({
overflow: visible;
}
.eli-tabela__tbody .eli-tabela__tr--zebra .eli-tabela__td {
background: rgba(15, 23, 42, 0.02);
}
.eli-tabela__th,
.eli-tabela__td {
padding: 10px 12px;
@ -786,6 +842,27 @@ export default defineComponent({
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 {
border: 1px solid rgba(220, 53, 69, 0.35);
border-radius: 12px;
@ -836,10 +913,11 @@ export default defineComponent({
}
.eli-tabela__cabecalho {
width: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
/* Ações ficam imediatamente à direita da busca (em vez de ir para a extrema direita) */
justify-content: flex-start;
justify-content: flex-end;
gap: 12px;
margin-bottom: 12px;
flex-wrap: wrap;

View file

@ -12,9 +12,11 @@
<button
type="button"
class="eli-tabela__busca-botao"
aria-label="Buscar"
title="Buscar"
@click="emitirBusca"
>
Buscar
<Search class="eli-tabela__busca-botao-icone" :size="16" :stroke-width="2" aria-hidden="true" />
</button>
</div>
</div>
@ -22,9 +24,11 @@
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
import { Search } from "lucide-vue-next";
export default defineComponent({
name: "EliTabelaCaixaDeBusca",
components: { Search },
props: {
modelo: {
type: String,
@ -65,10 +69,9 @@ export default defineComponent({
<style scoped>
.eli-tabela__busca {
display: flex;
align-items: flex-end;
align-items: center;
justify-content: flex-end;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
@ -95,6 +98,9 @@ export default defineComponent({
}
.eli-tabela__busca-botao {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
background: rgba(37, 99, 235, 0.12);
color: rgba(37, 99, 235, 0.95);
@ -103,6 +109,10 @@ export default defineComponent({
transition: background-color 0.2s ease, color 0.2s ease;
}
.eli-tabela__busca-botao-icone {
display: block;
}
.eli-tabela__busca-botao:hover,
.eli-tabela__busca-botao:focus-visible {
background: rgba(37, 99, 235, 0.2);

View file

@ -4,6 +4,8 @@ import type { VNodeChild } from "vue";
export type ComponenteCelula = VNodeChild;
export type EliAlinhamentoColuna = "esquerda" | "centro" | "direita";
export type EliColuna<T> = {
/** Texto exibido no cabeçalho da coluna. */
rotulo: string;
@ -11,6 +13,19 @@ export type EliColuna<T> = {
celula: (linha: T) => ComponenteCelula;
/** 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
* exibir controles de ordenação e utiliza o valor como chave para o backend.

View file

@ -25,7 +25,7 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { defineComponent } from "vue";
import BotaoPlayground from "./botao.playground.vue";
import IndicadorPlayground from "./indicador.playground.vue";
import CartaoPlayground from "./cartao.playground.vue";
@ -34,8 +34,37 @@ import DataHoraPlayground from "./data_hora.playground.vue";
import TabelaPlayground from "./tabela.playground.vue";
import OlaMundoPlayground from "./ola_mundo.playground.vue";
type AbaPlayground =
| "botao"
| "indicador"
| "cartao"
| "campo"
| "data_hora"
| "tabela"
| "ola_mundo";
const mapaHashParaAba: Record<string, AbaPlayground> = {
botao: "botao",
indicador: "indicador",
cartao: "cartao",
campo: "campo",
"data-hora": "data_hora",
tabela: "tabela",
demo: "ola_mundo",
};
const mapaAbaParaHash: Record<AbaPlayground, string> = {
botao: "botao",
indicador: "indicador",
cartao: "cartao",
campo: "campo",
data_hora: "data-hora",
tabela: "tabela",
ola_mundo: "demo",
};
export default defineComponent({
name: 'App',
name: "App",
components: {
BotaoPlayground,
IndicadorPlayground,
@ -47,15 +76,45 @@ export default defineComponent({
},
data() {
return {
aba: "botao" as
| "botao"
| "indicador"
| "cartao"
| "campo"
| "data_hora"
| "tabela"
| "ola_mundo",
aba: "botao" as AbaPlayground,
};
}
})
},
mounted() {
this.sincronizarAbaComHash();
window.addEventListener("hashchange", this.sincronizarAbaComHash);
},
beforeUnmount() {
window.removeEventListener("hashchange", this.sincronizarAbaComHash);
},
watch: {
aba() {
this.sincronizarHashComAba();
},
},
methods: {
normalizarHash() {
const valor = window.location.hash.replace(/^#\/?/, "").trim();
return valor;
},
sincronizarAbaComHash() {
const hash = this.normalizarHash();
const aba = mapaHashParaAba[hash] ?? "botao";
if (this.aba !== aba) {
this.aba = aba;
}
// Se a URL estiver vazia/fora do padrão, normalizamos.
this.sincronizarHashComAba();
},
sincronizarHashComAba() {
const destino = `#/${mapaAbaParaHash[this.aba]}`;
if (window.location.hash !== destino) {
window.location.hash = destino;
}
},
},
});
</script>

View file

@ -55,7 +55,7 @@ export default defineComponent({
empreendedor: "Maria Silva",
empreendimento: "Doces da Maria",
documento: "12.345.678/0001-90",
email: "contato@docesdamaria.com",
email: "contato.comercial.super.longo@doces-da-maria-exemplo-muito-grande.com.br",
telefone: "(11) 91234-5678",
},
{
@ -298,11 +298,14 @@ export default defineComponent({
rotulo: "Documento",
celula: (l) => l.documento,
coluna_ordem: "documento",
alinhamento: "direita",
},
{
rotulo: "E-mail",
celula: (l) => l.email,
coluna_ordem: "email",
truncar: true,
largura_maxima: 260,
acao: () => {
// Exemplo de ação: poderia abrir detalhes
console.log("Clicou na coluna e-mail");
@ -312,6 +315,7 @@ export default defineComponent({
rotulo: "Telefone",
celula: (l) => l.telefone,
coluna_ordem: "telefone",
alinhamento: "direita",
},
],
acoesLinha: acoesLinha,