diff --git a/.agent b/.agent index 13ae9ca..cdeaaaa 100644 --- a/.agent +++ b/.agent @@ -40,27 +40,6 @@ 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 - -``` - ---- - ## Estrutura obrigatória do repositório - Cada componente deve possuir **sua própria pasta** em `src/componentes/` - Dentro de cada pasta do componente: @@ -183,24 +162,6 @@ 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) ### Regra de publicação (sem usar `package.json.files`) diff --git a/IA.md b/IA.md index 9992243..3d9d871 100644 --- a/IA.md +++ b/IA.md @@ -61,15 +61,7 @@ createApp(App) ### 2) Importação direta (quando não quiser plugin) ```ts -import { - EliBotao, - EliBadge, - EliCartao, - EliTabela, - EliEntradaTexto, - EliEntradaNumero, - EliEntradaDataHora, -} from "eli-vue"; +import { EliBotao, EliInput, EliBadge, EliCartao, EliDataHora } from "eli-vue"; ``` > Observação: ainda pode ser necessário importar o CSS do pacote: @@ -90,18 +82,11 @@ import "eli-vue/dist/eli-vue.css"; ``` -### Entradas (EliEntrada*) com v-model - -O `eli-vue` usa uma família de componentes `EliEntrada*` (em vez do antigo `EliInput`). - -#### Texto +### Input com v-model ```vue ``` -#### Texto com formato/máscara +### Input de porcentagem -> Regra importante: o `value` emitido é **sempre o texto formatado** (igual ao que aparece no input). +Quando precisar de um campo numérico com sufixo `%`, use `type="porcentagem"`. ```vue @@ -150,10 +131,10 @@ export default defineComponent({ ```vue ``` -### EliEntradaNumero (exemplo) +### EliInput (porcentagem) + +Use `type="porcentagem"` quando precisar de um campo numérico com sufixo `%` embutido. ```vue - - diff --git a/src/componentes/EliEntrada/EliEntradaNumero.vue b/src/componentes/EliEntrada/EliEntradaNumero.vue deleted file mode 100644 index cd27780..0000000 --- a/src/componentes/EliEntrada/EliEntradaNumero.vue +++ /dev/null @@ -1,236 +0,0 @@ - - - - - diff --git a/src/componentes/EliEntrada/EliEntradaTexto.vue b/src/componentes/EliEntrada/EliEntradaTexto.vue deleted file mode 100644 index 9235593..0000000 --- a/src/componentes/EliEntrada/EliEntradaTexto.vue +++ /dev/null @@ -1,101 +0,0 @@ - - - - - diff --git a/src/componentes/EliEntrada/README.md b/src/componentes/EliEntrada/README.md deleted file mode 100644 index 2d2ee12..0000000 --- a/src/componentes/EliEntrada/README.md +++ /dev/null @@ -1,176 +0,0 @@ -# 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 - - - -``` - ---- - -#### 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 - - - -``` - ---- - -#### 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 - -``` - -### 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` diff --git a/src/componentes/EliEntrada/index.ts b/src/componentes/EliEntrada/index.ts deleted file mode 100644 index 55ac506..0000000 --- a/src/componentes/EliEntrada/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -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"; diff --git a/src/componentes/EliEntrada/registryEliEntradas.ts b/src/componentes/EliEntrada/registryEliEntradas.ts deleted file mode 100644 index 99e21c6..0000000 --- a/src/componentes/EliEntrada/registryEliEntradas.ts +++ /dev/null @@ -1,13 +0,0 @@ -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; diff --git a/src/componentes/EliEntrada/tiposEntradas.ts b/src/componentes/EliEntrada/tiposEntradas.ts deleted file mode 100644 index 5dd16c6..0000000 --- a/src/componentes/EliEntrada/tiposEntradas.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * 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 = {}> = { - /** 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 = - readonly [T, PadroesEntradas[T]['opcoes']] - -export type ComponenteEntrada = { - [K in TipoEntrada]: PadraoComponenteEntrada -}[TipoEntrada] \ No newline at end of file diff --git a/src/componentes/EliEntrada/utils/cep.ts b/src/componentes/EliEntrada/utils/cep.ts deleted file mode 100644 index 9eb4cb6..0000000 --- a/src/componentes/EliEntrada/utils/cep.ts +++ /dev/null @@ -1,10 +0,0 @@ -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); -} diff --git a/src/componentes/EliTabela/EliTabelaModalFiltroAvancado.vue b/src/componentes/EliTabela/EliTabelaModalFiltroAvancado.vue deleted file mode 100644 index 5fe7435..0000000 --- a/src/componentes/EliTabela/EliTabelaModalFiltroAvancado.vue +++ /dev/null @@ -1,391 +0,0 @@ - - - - - diff --git a/src/componentes/EliTabela/celulas/EliTabelaCelulaNumero.vue b/src/componentes/EliTabela/celulas/EliTabelaCelulaNumero.vue deleted file mode 100644 index 37c301b..0000000 --- a/src/componentes/EliTabela/celulas/EliTabelaCelulaNumero.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - - - diff --git a/src/componentes/EliTabela/celulas/EliTabelaCelulaTextoSimples.vue b/src/componentes/EliTabela/celulas/EliTabelaCelulaTextoSimples.vue deleted file mode 100644 index 92e39e1..0000000 --- a/src/componentes/EliTabela/celulas/EliTabelaCelulaTextoSimples.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - - - diff --git a/src/componentes/EliTabela/celulas/EliTabelaCelulaTextoTruncado.vue b/src/componentes/EliTabela/celulas/EliTabelaCelulaTextoTruncado.vue deleted file mode 100644 index b887977..0000000 --- a/src/componentes/EliTabela/celulas/EliTabelaCelulaTextoTruncado.vue +++ /dev/null @@ -1,63 +0,0 @@ - - - - - diff --git a/src/componentes/EliTabela/filtroAvancadoStorage.ts b/src/componentes/EliTabela/filtroAvancadoStorage.ts deleted file mode 100644 index 33fb34e..0000000 --- a/src/componentes/EliTabela/filtroAvancadoStorage.ts +++ /dev/null @@ -1,35 +0,0 @@ -export type EliTabelaFiltroAvancadoSalvo = Array<{ - coluna: keyof T - valor: any -}>; - -function key(nomeTabela: string) { - return `eli_tabela:${nomeTabela}:filtro_avancado`; -} - -export function carregarFiltroAvancado(nomeTabela: string): EliTabelaFiltroAvancadoSalvo { - try { - const raw = localStorage.getItem(key(nomeTabela)); - if (!raw) return [] as unknown as EliTabelaFiltroAvancadoSalvo; - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? (parsed as EliTabelaFiltroAvancadoSalvo) : ([] as any); - } catch { - return [] as unknown as EliTabelaFiltroAvancadoSalvo; - } -} - -export function salvarFiltroAvancado(nomeTabela: string, filtros: EliTabelaFiltroAvancadoSalvo) { - try { - localStorage.setItem(key(nomeTabela), JSON.stringify(filtros ?? [])); - } catch { - // ignore - } -} - -export function limparFiltroAvancado(nomeTabela: string) { - try { - localStorage.removeItem(key(nomeTabela)); - } catch { - // ignore - } -} diff --git a/src/componentes/campo/EliInput.vue b/src/componentes/campo/EliInput.vue new file mode 100644 index 0000000..59ca01f --- /dev/null +++ b/src/componentes/campo/EliInput.vue @@ -0,0 +1,304 @@ + + + + + diff --git a/src/componentes/campo/README.md b/src/componentes/campo/README.md new file mode 100644 index 0000000..6acd0a3 --- /dev/null +++ b/src/componentes/campo/README.md @@ -0,0 +1,118 @@ +# 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 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 + +``` + +--- + +## 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. diff --git a/src/componentes/campo/index.ts b/src/componentes/campo/index.ts new file mode 100644 index 0000000..890a77a --- /dev/null +++ b/src/componentes/campo/index.ts @@ -0,0 +1 @@ +export { default as EliInput } from "./EliInput.vue"; diff --git a/src/componentes/campo/utils/cep.ts b/src/componentes/campo/utils/cep.ts new file mode 100644 index 0000000..701fd34 --- /dev/null +++ b/src/componentes/campo/utils/cep.ts @@ -0,0 +1,9 @@ +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"); +} diff --git a/src/componentes/EliEntrada/utils/cpfCnpj.ts b/src/componentes/campo/utils/cpfCnpj.ts similarity index 100% rename from src/componentes/EliEntrada/utils/cpfCnpj.ts rename to src/componentes/campo/utils/cpfCnpj.ts diff --git a/src/componentes/campo/utils/numerico.ts b/src/componentes/campo/utils/numerico.ts new file mode 100644 index 0000000..5c2a3a6 --- /dev/null +++ b/src/componentes/campo/utils/numerico.ts @@ -0,0 +1,26 @@ +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, "."); +} diff --git a/src/componentes/EliEntrada/utils/telefone.ts b/src/componentes/campo/utils/telefone.ts similarity index 96% rename from src/componentes/EliEntrada/utils/telefone.ts rename to src/componentes/campo/utils/telefone.ts index dcd7d04..66fbd46 100644 --- a/src/componentes/EliEntrada/utils/telefone.ts +++ b/src/componentes/campo/utils/telefone.ts @@ -1,3 +1,5 @@ +// utils/telefone.ts + /** * Remove tudo que não é número */ diff --git a/src/componentes/data_hora/EliDataHora.vue b/src/componentes/data_hora/EliDataHora.vue new file mode 100644 index 0000000..ab6ec70 --- /dev/null +++ b/src/componentes/data_hora/EliDataHora.vue @@ -0,0 +1,231 @@ + + + + + + diff --git a/src/componentes/data_hora/README.md b/src/componentes/data_hora/README.md new file mode 100644 index 0000000..693b91a --- /dev/null +++ b/src/componentes/data_hora/README.md @@ -0,0 +1,108 @@ +# 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 + + + +``` + +### 2) Com limites (min/max) e validação visual + +```vue + +``` + +## 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). diff --git a/src/componentes/data_hora/index.ts b/src/componentes/data_hora/index.ts new file mode 100644 index 0000000..002d336 --- /dev/null +++ b/src/componentes/data_hora/index.ts @@ -0,0 +1 @@ +export { default as EliDataHora } from "./EliDataHora.vue"; \ No newline at end of file diff --git a/src/componentes/ola_mundo/EliOlaMundo.vue b/src/componentes/ola_mundo/EliOlaMundo.vue index 04a0cec..7289172 100644 --- a/src/componentes/ola_mundo/EliOlaMundo.vue +++ b/src/componentes/ola_mundo/EliOlaMundo.vue @@ -11,31 +11,100 @@
- - - - - + + + + - + + + + + + + + + + + + + + + + + +
@@ -52,27 +121,45 @@ import { defineComponent, ref } from "vue"; import EliBotao from "../botao/EliBotao.vue"; import EliBadge from "../indicador/EliBadge.vue"; -import EliEntradaTexto from "../EliEntrada/EliEntradaTexto.vue"; +import EliInput from "../campo/EliInput.vue"; + +type Habilidade = "vue" | "react"; export default defineComponent({ name: "EliOlaMundo", components: { EliBotao, EliBadge, - EliEntradaTexto, + EliInput, }, setup() { const nome = ref(""); + const estado = ref([]); const cep = ref(""); const telefone = ref(""); + const idade = ref(""); + const altura = ref(""); + const valor = ref(""); const email = ref(""); + const mensagem = ref(""); + const senha = ref(""); const documento = ref(""); + const cor = ref<"azul" | "verde" | null>(null); + const habilidades = ref([]); return { nome, email, documento, + estado, telefone, + mensagem, + senha, + cor, + habilidades, + idade, + altura, cep, + valor, }; }, }); diff --git a/src/componentes/ola_mundo/README.md b/src/componentes/ola_mundo/README.md index a4de0bb..7c057d9 100644 --- a/src/componentes/ola_mundo/README.md +++ b/src/componentes/ola_mundo/README.md @@ -2,14 +2,14 @@ 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 `EliEntradaTexto`, `EliEntradaNumero`, `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 `EliInput`, `EliBotao` e `EliBadge`. ## Estrutura do repositório (padrão) Neste Design System: -- **Pastas e arquivos** (quando aplicável) preferem português: `botao/`, `EliEntrada/`, `indicador/`, etc. -- **Componentes** mantêm prefixo técnico `Eli` (PascalCase): `EliBotao`, `EliEntradaTexto`. +- **Pastas e arquivos** (quando aplicável) preferem português: `botao/`, `campo/`, `indicador/`, etc. +- **Componentes** mantêm prefixo técnico `Eli` (PascalCase): `EliBotao`, `EliInput`. Exemplo: @@ -19,10 +19,9 @@ src/componentes/ EliBotao.vue index.ts README.md - EliEntrada/ - EliEntradaTexto.vue - EliEntradaNumero.vue - EliEntradaDataHora.vue + campo/ + EliInput.vue + index.ts README.md ``` diff --git a/src/componentes/EliTabela/EliTabela.css b/src/components/eli/EliTabela/EliTabela.css similarity index 95% rename from src/componentes/EliTabela/EliTabela.css rename to src/components/eli/EliTabela/EliTabela.css index cbc5f4a..9d2563a 100644 --- a/src/componentes/EliTabela/EliTabela.css +++ b/src/components/eli/EliTabela/EliTabela.css @@ -95,7 +95,26 @@ 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); diff --git a/src/componentes/EliTabela/EliTabela.vue b/src/components/eli/EliTabela/EliTabela.vue similarity index 72% rename from src/componentes/EliTabela/EliTabela.vue rename to src/components/eli/EliTabela/EliTabela.vue index a3c37f4..f12f586 100644 --- a/src/componentes/EliTabela/EliTabela.vue +++ b/src/components/eli/EliTabela/EliTabela.vue @@ -13,32 +13,20 @@ - - (null); const direcaoOrdenacao = ref<"asc" | "desc">("asc"); - /** Filtro avançado (config + estado modal) */ - const modalFiltroAberto = ref(false); - type LinhaFiltroUI = { - coluna: keyof T; - valor: any; - }; - - const filtrosUi = ref>>(carregarFiltroAvancado(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(() => { - // 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 */ const tabela = computed(() => props.tabela); @@ -243,20 +171,11 @@ export default defineComponent({ const colunasInvisiveisEfetivas = computed(() => { const colunas = props.tabela.colunas as Array>; - - 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 invisiveisSet = new Set(configColunas.value.invisiveis ?? []); const base = colunas.filter((c) => invisiveisSet.has(c.rotulo)); - // ordenação: usa a lista (salva ou derivada do default) e adiciona novas ao final - const ordemSalva = invisiveisBaseRotulos; + // ordenação: usa a lista salva de invisíveis (se existir), senão segue ordem original + const ordemSalva = configColunas.value.invisiveis ?? []; const mapa = new Map>(); for (const c of base) { if (!mapa.has(c.rotulo)) mapa.set(c.rotulo, c); @@ -279,25 +198,14 @@ export default defineComponent({ const colunasEfetivas = computed(() => { const colunas = props.tabela.colunas; const todosRotulos = rotulosColunas.value; + 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 ?? [] - : (props.tabela.colunas as Array>) - .filter((c) => c.visivel === false) - .map((c) => c.rotulo); - - const invisiveisSet = new Set(invisiveisBaseRotulos); - - // base visiveis: remove invisiveis + // default: todas visiveis; so some se estiver explicitamente em invisiveis const visiveisBaseRotulos = todosRotulos.filter((r) => !invisiveisSet.has(r)); const visiveisSet = new Set(visiveisBaseRotulos); // aplica ordem salva; novas (sem definicao) entram no fim, respeitando ordem original - const ordemSalva = configTemDados ? configColunas.value.visiveis ?? [] : []; + const ordemSalva = configColunas.value.visiveis ?? []; const ordemFinal: string[] = []; for (const r of ordemSalva) { @@ -351,91 +259,20 @@ export default defineComponent({ return 10; }); - 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 */ + /** Total de páginas calculado com base na quantidade */ const totalPaginas = computed(() => { const limite = registrosPorConsulta.value; if (!limite || limite <= 0) return 1; - const total = quantidadeFiltrada.value; + const total = quantidade.value; if (!total) return 1; 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 */ const temAcoes = computed(() => (props.tabela.acoesLinha ?? []).length > 0); - const exibirFiltroAvancado = computed(() => (props.tabela.filtroAvancado ?? []).length > 0); - /** Sequencial para evitar race conditions entre consultas */ let carregamentoSequencial = 0; @@ -594,10 +431,8 @@ export default defineComponent({ menuAberto.value = null; 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 offset = 0; + const offset = (paginaAtual.value - 1) * limite; const parametrosConsulta: { coluna_ordem?: never; @@ -607,10 +442,12 @@ export default defineComponent({ texto_busca?: string; } = { offSet: offset, - limit: 999999, + limit: limite, }; - // texto_busca ficará somente para filtragem local. + if (valorBusca.value) { + parametrosConsulta.texto_busca = valorBusca.value; + } if (colunaOrdenacao.value) { parametrosConsulta.coluna_ordem = colunaOrdenacao.value as never; @@ -631,12 +468,12 @@ export default defineComponent({ } const valores = res.valor?.valores ?? []; - const total = valores.length; + const total = res.valor?.quantidade ?? valores.length; linhas.value = valores; quantidade.value = total; - const totalPaginasRecalculado = Math.max(1, Math.ceil((quantidadeFiltrada.value || 0) / limite)); + const totalPaginasRecalculado = Math.max(1, Math.ceil((total || 0) / limite)); if (paginaAtual.value > totalPaginasRecalculado) { paginaAtual.value = totalPaginasRecalculado; return; @@ -720,10 +557,7 @@ export default defineComponent({ /** Watch: mudança de página dispara nova consulta */ watch(paginaAtual, (nova, antiga) => { - // paginação local não precisa recarregar - if (nova !== antiga) { - // noop - } + if (nova !== antiga) void carregar(); }); /** Watch: troca de configuração reseta estados e recarrega */ @@ -735,9 +569,7 @@ export default defineComponent({ direcaoOrdenacao.value = "asc"; valorBusca.value = ""; modalColunasAberto.value = false; - modalFiltroAberto.value = false; configColunas.value = carregarConfigColunas(props.tabela.nome); - filtrosUi.value = carregarFiltroAvancado(props.tabela.nome) as any; linhasExpandidas.value = {}; if (paginaAtual.value !== 1) { paginaAtual.value = 1; @@ -773,8 +605,6 @@ export default defineComponent({ carregando, erro, linhas, - linhasPaginadas, - quantidadeFiltrada, quantidade, menuAberto, valorBusca, @@ -785,7 +615,6 @@ export default defineComponent({ // computed exibirBusca, - exibirFiltroAvancado, acoesCabecalho, temAcoesCabecalho, temAcoes, @@ -799,16 +628,9 @@ export default defineComponent({ linhasExpandidas, abrirModalColunas, - abrirModalFiltro, fecharModalColunas, salvarModalColunas, - modalFiltroAberto, - filtrosUi, - salvarFiltrosAvancados, - limparFiltrosAvancados, - fecharModalFiltro, - alternarLinhaExpandida, // actions diff --git a/src/componentes/EliTabela/EliTabelaBody.vue b/src/components/eli/EliTabela/EliTabelaBody.vue similarity index 72% rename from src/componentes/EliTabela/EliTabelaBody.vue rename to src/components/eli/EliTabela/EliTabelaBody.vue index 133bd27..adadd9f 100644 --- a/src/componentes/EliTabela/EliTabelaBody.vue +++ b/src/components/eli/EliTabela/EliTabelaBody.vue @@ -29,8 +29,22 @@ v-for="(coluna, j) in colunas" :key="`td-${i}-${j}`" class="eli-tabela__td" + :class="[ + coluna.acao ? 'eli-tabela__td--clicavel' : undefined, + obterClasseAlinhamento(coluna.alinhamento), + ]" + @click="coluna.acao ? () => coluna.acao?.() : undefined" > - + + + + +
@@ -131,9 +145,40 @@ export default defineComponent({ }, }, 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 { ChevronRight, ChevronDown, + obterClasseAlinhamento, + obterMaxWidth, + obterTooltipCelula, }; }, }); diff --git a/src/componentes/EliTabela/EliTabelaCabecalho.vue b/src/components/eli/EliTabela/EliTabelaCabecalho.vue similarity index 81% rename from src/componentes/EliTabela/EliTabelaCabecalho.vue rename to src/components/eli/EliTabela/EliTabelaCabecalho.vue index 7a8b66e..c3a4865 100644 --- a/src/componentes/EliTabela/EliTabelaCabecalho.vue +++ b/src/components/eli/EliTabela/EliTabelaCabecalho.vue @@ -10,15 +10,6 @@ > Colunas - - @@ -62,11 +53,6 @@ export default defineComponent({ required: false, default: true, }, - exibirBotaoFiltroAvancado: { - type: Boolean, - required: false, - default: false, - }, valorBusca: { type: String, required: true, @@ -90,9 +76,6 @@ export default defineComponent({ colunas() { return true; }, - filtroAvancado() { - return true; - }, }, setup(props, { emit }) { const temAcoesCabecalho = computed(() => props.acoesCabecalho.length > 0); @@ -105,11 +88,7 @@ export default defineComponent({ emit("colunas"); } - function emitFiltroAvancado() { - emit("filtroAvancado"); - } - - return { temAcoesCabecalho, emitBuscar, emitColunas, emitFiltroAvancado }; + return { temAcoesCabecalho, emitBuscar, emitColunas }; }, }); diff --git a/src/componentes/EliTabela/EliTabelaCaixaDeBusca.vue b/src/components/eli/EliTabela/EliTabelaCaixaDeBusca.vue similarity index 100% rename from src/componentes/EliTabela/EliTabelaCaixaDeBusca.vue rename to src/components/eli/EliTabela/EliTabelaCaixaDeBusca.vue diff --git a/src/componentes/EliTabela/EliTabelaDebug.vue b/src/components/eli/EliTabela/EliTabelaDebug.vue similarity index 100% rename from src/componentes/EliTabela/EliTabelaDebug.vue rename to src/components/eli/EliTabela/EliTabelaDebug.vue diff --git a/src/componentes/EliTabela/EliTabelaDetalhesLinha.vue b/src/components/eli/EliTabela/EliTabelaDetalhesLinha.vue similarity index 100% rename from src/componentes/EliTabela/EliTabelaDetalhesLinha.vue rename to src/components/eli/EliTabela/EliTabelaDetalhesLinha.vue diff --git a/src/componentes/EliTabela/EliTabelaEstados.vue b/src/components/eli/EliTabela/EliTabelaEstados.vue similarity index 100% rename from src/componentes/EliTabela/EliTabelaEstados.vue rename to src/components/eli/EliTabela/EliTabelaEstados.vue diff --git a/src/componentes/EliTabela/EliTabelaHead.vue b/src/components/eli/EliTabela/EliTabelaHead.vue similarity index 85% rename from src/componentes/EliTabela/EliTabelaHead.vue rename to src/components/eli/EliTabela/EliTabelaHead.vue index ed314a6..0cd741b 100644 --- a/src/componentes/EliTabela/EliTabelaHead.vue +++ b/src/components/eli/EliTabela/EliTabelaHead.vue @@ -7,7 +7,10 @@ v-for="(coluna, idx) in colunas" :key="`th-${idx}`" class="eli-tabela__th" - :class="[isOrdenavel(coluna) ? 'eli-tabela__th--ordenavel' : undefined]" + :class="[ + isOrdenavel(coluna) ? 'eli-tabela__th--ordenavel' : undefined, + obterClasseAlinhamento(coluna.alinhamento), + ]" scope="col" >