Compare commits
11 commits
751857b170
...
5bb6732b81
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bb6732b81 | |||
| ab15c51a1b | |||
| 638fca78b0 | |||
| 5d78d648e1 | |||
| 7c274583ec | |||
| eb4538f4ba | |||
| 51a48eee70 | |||
| 86d451efa1 | |||
| 4fdc5a95ce | |||
| 6c84508996 | |||
| 454fddb061 |
44 changed files with 2501 additions and 199 deletions
197
.agent
Normal file
197
.agent
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
# Design System Vue 3 (TypeScript) — Regras do Agente
|
||||||
|
|
||||||
|
## Objetivo do projeto
|
||||||
|
Construir um Design System de componentes em **Vue 3** para reutilização em múltiplos projetos, com foco em:
|
||||||
|
- **Consistência visual e de comportamento**
|
||||||
|
- **Componentes bem tipados (TypeScript forte)**
|
||||||
|
- **Documentação em português** (para devs e para outras IAs)
|
||||||
|
- **Exemplos executáveis (playground)**
|
||||||
|
- **Facilidade de manutenção e evolução**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## O que NÃO entra no contexto do agente
|
||||||
|
|
||||||
|
- `node_modules/`: dependências da arquitetura (não versionar / não usar como “fonte da verdade”).
|
||||||
|
- `dist/`: pasta **gerada** pelo build (não versionar). Use para validar o build localmente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stack e padrões obrigatórios
|
||||||
|
- Vue 3
|
||||||
|
- TypeScript (modo estrito e tipagem forte)
|
||||||
|
- **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)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regras de idioma e nomenclatura
|
||||||
|
- **Variáveis, nomes de arquivos e nomes de pastas em português sempre que possível**
|
||||||
|
- Ex.: `botao`, `cartao`, `campo_texto`, `seletor_opcoes`
|
||||||
|
- Evitar abreviações confusas
|
||||||
|
- Nomes de componentes (PascalCase) podem seguir padrão técnico:
|
||||||
|
- **Padrão do repositório:** componentes com prefixo `Eli` (ex.: `EliBotao`, `EliInput`).
|
||||||
|
- Pastas preferem português (ex.: `src/componentes/botao/`, `src/componentes/campo/`).
|
||||||
|
- Props e eventos: preferir português e sem ambiguidades:
|
||||||
|
- Props: `rotulo`, `desabilitado`, `carregando`, `modeloValor`
|
||||||
|
- Eventos: `update:modelValue`, `confirmar`, `cancelar`, `clicar`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estrutura obrigatória do repositório
|
||||||
|
- Cada componente deve possuir **sua própria pasta** em `src/componentes/`
|
||||||
|
- Dentro de cada pasta do componente:
|
||||||
|
- `README.md` (detalhado e em português)
|
||||||
|
- `index.ts` (re-export)
|
||||||
|
- Arquivo do componente `.vue`
|
||||||
|
- (opcional) `tipos.ts` se necessário para manter tipagem limpa
|
||||||
|
|
||||||
|
Estrutura sugerida:
|
||||||
|
|
||||||
|
src/
|
||||||
|
componentes/
|
||||||
|
botao/
|
||||||
|
EliBotao.vue
|
||||||
|
index.ts
|
||||||
|
README.md
|
||||||
|
cartao/
|
||||||
|
EliCartao.vue
|
||||||
|
index.ts
|
||||||
|
README.md
|
||||||
|
playground/
|
||||||
|
botao.playground.vue
|
||||||
|
cartao.playground.vue
|
||||||
|
index.ts
|
||||||
|
|
||||||
|
Raiz do projeto:
|
||||||
|
- `README.md` geral (guia do design system em português)
|
||||||
|
|
||||||
|
Pastas geradas/ignoradas:
|
||||||
|
- `dist/` (build)
|
||||||
|
- `node_modules/` (dependências)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentação obrigatória (README.md)
|
||||||
|
### 1) README.md na raiz (obrigatório)
|
||||||
|
Deve servir como guia para desenvolvedores e outras IAs, contendo:
|
||||||
|
- Visão geral do design system
|
||||||
|
- Como instalar e usar (ex.: importações, registro)
|
||||||
|
- Convenções do projeto (tipagem, pastas, playground)
|
||||||
|
- Como criar um novo componente (checklist)
|
||||||
|
- Como rodar o playground
|
||||||
|
- Padrões de versionamento (se aplicável)
|
||||||
|
|
||||||
|
### 2) README.md por componente (obrigatório)
|
||||||
|
Cada componente precisa ter um README.md com **mais detalhamento**:
|
||||||
|
- O que o componente resolve (objetivo)
|
||||||
|
- API completa:
|
||||||
|
- Props (nome, tipo, padrão, descrição)
|
||||||
|
- Emits (nome, payload, quando dispara)
|
||||||
|
- Slots (nome, objetivo, exemplo)
|
||||||
|
- Exemplos de uso (mínimo 2)
|
||||||
|
- Casos de borda / comportamento esperado
|
||||||
|
- Acessibilidade (quando aplicável)
|
||||||
|
- Decisões de implementação relevantes (por que foi feito assim)
|
||||||
|
|
||||||
|
**Idioma:** sempre em português claro.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comentários obrigatórios nos componentes
|
||||||
|
Todos os componentes `.vue` devem conter comentários úteis, principalmente:
|
||||||
|
- Explicando a intenção de blocos importantes (watchers, computed, validações)
|
||||||
|
- Explicando decisões de tipagem
|
||||||
|
- Explicando regras de negócio / estados (carregando, erro, desabilitado)
|
||||||
|
Evitar comentários óbvios (“isso é um botão”).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tipagem forte (TypeScript) — regras
|
||||||
|
- Usar `strict: true`
|
||||||
|
- Evitar `any` (proibido), exceto se for inevitável e justificado no README do componente
|
||||||
|
- Preferir tipos explícitos para:
|
||||||
|
- props complexas
|
||||||
|
- retornos de funções
|
||||||
|
- emits com payload tipado
|
||||||
|
- Para props complexas, usar `PropType<T>` quando necessário
|
||||||
|
- Padronizar nomes de tipos em português:
|
||||||
|
- `tipoOpcao`, `tipoTamanho`, `tipoEstado`, `tipoTema`
|
||||||
|
|
||||||
|
### Centralização de tipos (padrão do repositório)
|
||||||
|
|
||||||
|
- Tipos compartilhados (uniões, enums, aliases) devem ficar em `src/tipos/`.
|
||||||
|
- Cada domínio pode ter seu arquivo:
|
||||||
|
- `src/tipos/botao.ts`
|
||||||
|
- `src/tipos/campo.ts`
|
||||||
|
- `src/tipos/indicador.ts`
|
||||||
|
- Re-export central em `src/tipos/index.ts`.
|
||||||
|
- Componentes importam tipagens de `src/tipos`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Padrão defineComponent (obrigatório)
|
||||||
|
- Sempre usar:
|
||||||
|
- `<script lang="ts">`
|
||||||
|
- `export default defineComponent({ ... })`
|
||||||
|
- Props com tipos explícitos
|
||||||
|
- Emits declarados e tipados
|
||||||
|
- Preferir `computed`, `ref` e `watch` com tipos claros
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Playground obrigatório para cada componente
|
||||||
|
- Todo componente criado deve ter **um teste demonstrativo** em `src/playground`
|
||||||
|
- O playground deve:
|
||||||
|
- Exibir variações do componente (mínimo 3 variações)
|
||||||
|
- Demonstrar interação (v-model, click, estados)
|
||||||
|
- Validar visualmente comportamentos e casos de borda
|
||||||
|
- Nome do arquivo do playground:
|
||||||
|
- `nome_do_componente.playground.vue`
|
||||||
|
- Ex.: `botao.playground.vue`, `campo_texto.playground.vue`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regras de exportação e reuso
|
||||||
|
- Cada pasta do componente deve ter `index.ts` exportando o componente:
|
||||||
|
- `export { default as EliBotao } from "./EliBotao.vue"`
|
||||||
|
- `src/index.ts` deve exportar todos os componentes publicamente
|
||||||
|
- Não exportar itens internos não documentados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Qualidade e consistência
|
||||||
|
- Antes de finalizar um componente:
|
||||||
|
- Conferir documentação (README raiz + README do componente)
|
||||||
|
- Conferir comentários no `.vue`
|
||||||
|
- Conferir playground criado e funcionando
|
||||||
|
- Conferir tipagem (sem any; sem tipos implícitos perigosos)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist por componente (obrigatório)
|
||||||
|
Ao criar/alterar componente, garantir:
|
||||||
|
- [ ] Pasta do componente criada em `src/componentes/<nome_em_portugues>/`
|
||||||
|
- [ ] `.vue` com defineComponent + comentários úteis
|
||||||
|
- [ ] `index.ts` com export do componente
|
||||||
|
- [ ] README.md do componente completo (em português)
|
||||||
|
- [ ] Playground criado em `src/playground/`
|
||||||
|
- [ ] Export adicionado em `src/index.ts`
|
||||||
|
- [ ] Tipos fortes (sem any; PropType<T> quando necessário)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regra de mudança importante
|
||||||
|
Sempre que uma mudança for relevante para desenvolvedores/usuários/IA, **atualizar o README.md correspondente**:
|
||||||
|
- README da raiz (se impactar o projeto)
|
||||||
|
- README do componente (se impactar API/uso/comportamento)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Postura do agente ao gerar código
|
||||||
|
- Gerar código enxuto, legível e consistente
|
||||||
|
- Priorizar clareza e tipagem forte
|
||||||
|
- Nunca “inventar” API sem documentar
|
||||||
|
- Se houver dúvida de padrão visual, criar uma implementação neutra, documentar e manter extensível
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
node_modules
|
node_modules
|
||||||
|
dist
|
||||||
|
|
|
||||||
461
README.md
461
README.md
|
|
@ -1,5 +1,460 @@
|
||||||
# Vue 3 + TypeScript + Vite
|
# eli-vue — Design System (Vue 3 + TypeScript)
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
Biblioteca de componentes Vue 3 (Design System) para reutilização em múltiplos projetos.
|
||||||
|
|
||||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
Este README foi escrito para **humanos e IAs**: ele descreve o objetivo do repositório, regras, estrutura, fluxo de contribuição, comandos e um roadmap de melhorias.
|
||||||
|
|
||||||
|
## Fonte da verdade (regras)
|
||||||
|
|
||||||
|
As regras oficiais do repositório estão no arquivo **`.agent`**.
|
||||||
|
|
||||||
|
Antes de propor mudanças:
|
||||||
|
|
||||||
|
- leia o `.agent`
|
||||||
|
- procure padrões já existentes no código
|
||||||
|
- atualize a documentação correspondente (README raiz e/ou README do componente)
|
||||||
|
|
||||||
|
## Objetivos
|
||||||
|
|
||||||
|
- consistência visual e comportamental
|
||||||
|
- tipagem forte (TypeScript `strict`)
|
||||||
|
- documentação em português
|
||||||
|
- exemplos executáveis via playground
|
||||||
|
|
||||||
|
## O que NÃO entra no contexto
|
||||||
|
|
||||||
|
- `node_modules/`: dependências (não versionar; não usar como fonte da verdade)
|
||||||
|
- `dist/`: gerado no build (não versionar)
|
||||||
|
|
||||||
|
## Arquitetura do repositório
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
componentes/
|
||||||
|
botao/
|
||||||
|
EliBotao.vue
|
||||||
|
index.ts
|
||||||
|
README.md
|
||||||
|
campo/
|
||||||
|
EliInput.vue
|
||||||
|
index.ts
|
||||||
|
README.md
|
||||||
|
indicador/
|
||||||
|
EliBadge.vue
|
||||||
|
index.ts
|
||||||
|
README.md
|
||||||
|
tipos/
|
||||||
|
botao.ts
|
||||||
|
campo.ts
|
||||||
|
indicador.ts
|
||||||
|
index.ts
|
||||||
|
playground/
|
||||||
|
App.vue
|
||||||
|
*.playground.vue
|
||||||
|
index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Convenções (nomenclatura)
|
||||||
|
|
||||||
|
- Componentes usam **prefixo `Eli`** (ex.: `EliBotao`, `EliInput`).
|
||||||
|
- Pastas preferem **português** (ex.: `src/componentes/botao`, `src/componentes/campo`).
|
||||||
|
- Tipos compartilhados ficam em `src/tipos/`.
|
||||||
|
- Sem TSX; padrão SFC: `<template>` + `<script lang="ts">` + `defineComponent`.
|
||||||
|
- `any` é proibido.
|
||||||
|
|
||||||
|
## Instalação
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add eli-vue
|
||||||
|
```
|
||||||
|
|
||||||
|
> Observação: `vue` e `vuetify` são **peerDependencies**.
|
||||||
|
|
||||||
|
### Instalação (projeto consumidor já usa Vuetify)
|
||||||
|
|
||||||
|
Se o seu projeto já está configurado com Vuetify 3, basta instalar este pacote:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add eli-vue
|
||||||
|
```
|
||||||
|
|
||||||
|
> Dica: mantenha `vue` e `vuetify` atualizados dentro das faixas suportadas.
|
||||||
|
|
||||||
|
## Uso
|
||||||
|
|
||||||
|
### 1) Registro global (plugin)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createApp } from "vue";
|
||||||
|
import EliVue from "eli-vue";
|
||||||
|
|
||||||
|
import App from "./App.vue";
|
||||||
|
|
||||||
|
createApp(App).use(EliVue).mount("#app");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Exemplo completo (main.ts) — com Vuetify 3
|
||||||
|
|
||||||
|
Este é o modelo recomendado para projetos consumidores (Vite + Vue 3) **que já usam Vuetify 3**:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createApp } from "vue";
|
||||||
|
import App from "./App.vue";
|
||||||
|
|
||||||
|
// Vuetify
|
||||||
|
import "vuetify/styles";
|
||||||
|
import { createVuetify } from "vuetify";
|
||||||
|
|
||||||
|
// eli-vue (Design System)
|
||||||
|
import EliVue from "eli-vue";
|
||||||
|
import "eli-vue/dist/eli-vue.css";
|
||||||
|
|
||||||
|
const vuetify = createVuetify({
|
||||||
|
// opcional: componentes/directives/tema do seu projeto
|
||||||
|
});
|
||||||
|
|
||||||
|
createApp(App)
|
||||||
|
.use(vuetify)
|
||||||
|
.use(EliVue)
|
||||||
|
.mount("#app");
|
||||||
|
```
|
||||||
|
|
||||||
|
> Nota: o `eli-vue` **não cria** nem configura Vuetify; ele assume que o consumidor já tem Vuetify.
|
||||||
|
|
||||||
|
### 2) Importação direta de componentes
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { EliBotao, EliInput, EliBadge } from "eli-vue";
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exemplos rápidos de uso
|
||||||
|
|
||||||
|
### EliBotao
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<EliBotao @click="salvar">Salvar</EliBotao>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### EliInput (v-model)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<EliInput v-model="nome" label="Nome" placeholder="Digite seu nome" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from "vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const nome = ref("");
|
||||||
|
return { nome };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### EliBadge
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<EliBadge badge="3">
|
||||||
|
<v-icon>mdi-bell</v-icon>
|
||||||
|
</EliBadge>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting (problemas comuns)
|
||||||
|
|
||||||
|
### 1) "Failed to resolve component" / componente não registrado
|
||||||
|
|
||||||
|
Você provavelmente esqueceu de:
|
||||||
|
|
||||||
|
- usar o plugin: `app.use(EliVue)`
|
||||||
|
- ou importar e registrar o componente localmente
|
||||||
|
|
||||||
|
### 2) Estilos não aplicam / layout estranho
|
||||||
|
|
||||||
|
Confirme que o consumidor importou:
|
||||||
|
|
||||||
|
- `vuetify/styles`
|
||||||
|
- `eli-vue/dist/eli-vue.css`
|
||||||
|
|
||||||
|
### 3) Ícones não aparecem
|
||||||
|
|
||||||
|
Alguns exemplos usam `<v-icon>` com Material Design Icons. O `eli-vue` **não instala ícones** no seu projeto.
|
||||||
|
|
||||||
|
- garanta que o seu projeto consumidor está configurado com o pacote de ícones desejado (ex.: MDI)
|
||||||
|
- ou substitua o conteúdo do slot por outro elemento
|
||||||
|
|
||||||
|
## Exemplo de composição (pipeline estilo Trello)
|
||||||
|
|
||||||
|
O caso mais comum no **gestor de oportunidades e propostas comerciais** é compor UI a partir de componentes básicos.
|
||||||
|
Exemplo: um **pipeline** em colunas (estilo Trello/Kanban) com cards de oportunidades.
|
||||||
|
|
||||||
|
> Este exemplo usa Vuetify para layout/containers e o `eli-vue` para inputs, botões e indicadores.
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<v-container class="py-6">
|
||||||
|
<div class="toolbar">
|
||||||
|
<h2 class="text-h6">Pipeline de Oportunidades</h2>
|
||||||
|
<EliInput
|
||||||
|
v-model="filtro"
|
||||||
|
label="Buscar"
|
||||||
|
placeholder="Cliente, proposta, valor..."
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
<EliBotao @click="criarOportunidade">Nova oportunidade</EliBotao>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="colunas">
|
||||||
|
<section
|
||||||
|
v-for="coluna in colunas"
|
||||||
|
:key="coluna.id"
|
||||||
|
class="coluna"
|
||||||
|
>
|
||||||
|
<header class="coluna-header">
|
||||||
|
<strong>{{ coluna.titulo }}</strong>
|
||||||
|
<EliBadge :badge="coluna.itens.length" radius="pill" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<v-card
|
||||||
|
v-for="item in itensFiltrados(coluna.itens)"
|
||||||
|
:key="item.id"
|
||||||
|
class="card"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<v-card-title class="d-flex justify-space-between align-center">
|
||||||
|
<span class="text-subtitle-2">{{ item.titulo }}</span>
|
||||||
|
<EliBadge
|
||||||
|
v-if="item.alerta"
|
||||||
|
dot
|
||||||
|
color="error"
|
||||||
|
:visible="true"
|
||||||
|
>
|
||||||
|
<span />
|
||||||
|
</EliBadge>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="text-body-2">
|
||||||
|
<div><strong>Cliente:</strong> {{ item.cliente }}</div>
|
||||||
|
<div><strong>Valor:</strong> {{ item.valor }}</div>
|
||||||
|
<div><strong>Vencimento:</strong> {{ item.vencimento }}</div>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<EliBotao variant="text" @click="abrir(item)">Abrir</EliBotao>
|
||||||
|
<EliBotao variant="text" color="secondary" @click="editar(item)">
|
||||||
|
Editar
|
||||||
|
</EliBotao>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from "vue";
|
||||||
|
import { EliBadge, EliBotao, EliInput } from "eli-vue";
|
||||||
|
|
||||||
|
type Oportunidade = {
|
||||||
|
id: string;
|
||||||
|
titulo: string;
|
||||||
|
cliente: string;
|
||||||
|
valor: string;
|
||||||
|
vencimento: string;
|
||||||
|
alerta?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Coluna = {
|
||||||
|
id: string;
|
||||||
|
titulo: string;
|
||||||
|
itens: Oportunidade[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "PipelineExemplo",
|
||||||
|
components: { EliBadge, EliBotao, EliInput },
|
||||||
|
setup() {
|
||||||
|
const filtro = ref("");
|
||||||
|
|
||||||
|
const colunas = ref<Coluna[]>([
|
||||||
|
{
|
||||||
|
id: "novo",
|
||||||
|
titulo: "Novo",
|
||||||
|
itens: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
titulo: "Proposta #1024",
|
||||||
|
cliente: "ACME Ltda",
|
||||||
|
valor: "R$ 12.500,00",
|
||||||
|
vencimento: "10/01/2026",
|
||||||
|
alerta: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "negociacao",
|
||||||
|
titulo: "Negociação",
|
||||||
|
itens: [
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
titulo: "Oportunidade #204",
|
||||||
|
cliente: "Empresa X",
|
||||||
|
valor: "R$ 8.000,00",
|
||||||
|
vencimento: "15/01/2026",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ganho",
|
||||||
|
titulo: "Ganho",
|
||||||
|
itens: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
function itensFiltrados(itens: Oportunidade[]) {
|
||||||
|
const q = filtro.value.trim().toLowerCase();
|
||||||
|
if (!q) return itens;
|
||||||
|
return itens.filter((i) =>
|
||||||
|
`${i.titulo} ${i.cliente} ${i.valor}`.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function criarOportunidade() {
|
||||||
|
// aqui você abriria um modal / rota de criação
|
||||||
|
}
|
||||||
|
|
||||||
|
function abrir(item: Oportunidade) {
|
||||||
|
// aqui você navega para detalhes da oportunidade
|
||||||
|
}
|
||||||
|
|
||||||
|
function editar(item: Oportunidade) {
|
||||||
|
// aqui você abre edição
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
filtro,
|
||||||
|
colunas,
|
||||||
|
itensFiltrados,
|
||||||
|
criarOportunidade,
|
||||||
|
abrir,
|
||||||
|
editar,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toolbar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr minmax(260px, 420px) auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colunas {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.coluna {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coluna-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.toolbar {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Como rodar localmente
|
||||||
|
|
||||||
|
### Playground
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
O playground (`src/playground`) valida visualmente variações e interações.
|
||||||
|
|
||||||
|
### Build da lib + geração de tipos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Gera `dist/` (artefatos de build) e `dist/types` (declarações `.d.ts`).
|
||||||
|
|
||||||
|
## Guia rápido para IAs (antes de codar)
|
||||||
|
|
||||||
|
1) **Leia** `.agent` e este README.
|
||||||
|
2) **Busque padrões** antes de criar API nova (props/emits/slots).
|
||||||
|
3) **Não use `any`**.
|
||||||
|
4) **Centralize tipos** em `src/tipos/`.
|
||||||
|
5) Ao mudar API/comportamento:
|
||||||
|
- atualize o `README.md` do componente
|
||||||
|
- atualize ou crie um `*.playground.vue`
|
||||||
|
6) Rode `pnpm run build` antes de finalizar.
|
||||||
|
|
||||||
|
## Como criar/alterar um componente (checklist)
|
||||||
|
|
||||||
|
1. Criar pasta em `src/componentes/<nome_em_portugues>/`
|
||||||
|
2. Criar `EliNomeDoComponente.vue` com `defineComponent` + comentários úteis
|
||||||
|
3. Criar `index.ts` com re-export (ex.: `export { default as EliX } from './EliX.vue'`)
|
||||||
|
4. Criar `README.md` do componente (API, exemplos, casos de borda, A11y)
|
||||||
|
5. Criar playground em `src/playground/<nome>.playground.vue` (mín. 3 variações)
|
||||||
|
6. Exportar no `src/index.ts`
|
||||||
|
|
||||||
|
### Critérios de aceite (qualidade)
|
||||||
|
|
||||||
|
- `pnpm run build` passa (inclui `vue-tsc`)
|
||||||
|
- zero `any`
|
||||||
|
- `defineComponent` em todos os componentes
|
||||||
|
- README do componente atualizado quando API mudar
|
||||||
|
- playground demonstrando variações + interação
|
||||||
|
|
||||||
|
## Roadmap de melhorias (a aplicar)
|
||||||
|
|
||||||
|
### Curto prazo (qualidade / consistência)
|
||||||
|
- [ ] Padronizar comentários úteis em todos os componentes (explicar decisões e estados)
|
||||||
|
- [ ] Tipar `emits` com validação de payload (quando houver)
|
||||||
|
- [ ] Consolidar nomenclatura de props/eventos em português (avaliar breaking changes)
|
||||||
|
|
||||||
|
### Médio prazo (DX + segurança)
|
||||||
|
- [ ] Adicionar ESLint + Prettier e regras (incluindo proibição de `any`)
|
||||||
|
- [ ] Adicionar testes unitários com Vitest + @vue/test-utils
|
||||||
|
- [ ] Criar pipeline de CI rodando `pnpm run build`
|
||||||
|
|
||||||
|
### Longo prazo (docs e maturidade)
|
||||||
|
- [ ] Documentação visual (Storybook ou VitePress)
|
||||||
|
- [ ] Tokens de design (cores/spacing) e guideline de acessibilidade
|
||||||
|
|
|
||||||
62
dist/eli-vue.es.js
vendored
62
dist/eli-vue.es.js
vendored
|
|
@ -1,62 +0,0 @@
|
||||||
import { defineComponent as u, createBlock as i, openBlock as f, withCtx as o, createVNode as n, createTextVNode as l } from "vue";
|
|
||||||
import { VBtn as m } from "vuetify/components/VBtn";
|
|
||||||
import { VCard as s, VCardTitle as p, VCardText as c, VCardActions as _ } from "vuetify/components/VCard";
|
|
||||||
import { VContainer as x } from "vuetify/components/VGrid";
|
|
||||||
const V = u({
|
|
||||||
name: "EliOlaMundo"
|
|
||||||
}), C = (e, t) => {
|
|
||||||
const r = e.__vccOpts || e;
|
|
||||||
for (const [a, d] of t)
|
|
||||||
r[a] = d;
|
|
||||||
return r;
|
|
||||||
};
|
|
||||||
function O(e, t, r, a, d, y) {
|
|
||||||
return f(), i(x, null, {
|
|
||||||
default: o(() => [
|
|
||||||
n(s, {
|
|
||||||
class: "mx-auto",
|
|
||||||
max_width: "400"
|
|
||||||
}, {
|
|
||||||
default: o(() => [
|
|
||||||
n(p, null, {
|
|
||||||
default: o(() => [...t[0] || (t[0] = [
|
|
||||||
l("Olá Mundo!", -1)
|
|
||||||
])]),
|
|
||||||
_: 1
|
|
||||||
}),
|
|
||||||
n(c, null, {
|
|
||||||
default: o(() => [...t[1] || (t[1] = [
|
|
||||||
l(" Este é um componente de exemplo integrado com Vuetify. ", -1)
|
|
||||||
])]),
|
|
||||||
_: 1
|
|
||||||
}),
|
|
||||||
n(_, null, {
|
|
||||||
default: o(() => [
|
|
||||||
n(m, {
|
|
||||||
color: "primary",
|
|
||||||
block: ""
|
|
||||||
}, {
|
|
||||||
default: o(() => [...t[2] || (t[2] = [
|
|
||||||
l(" Botão Vuetify ", -1)
|
|
||||||
])]),
|
|
||||||
_: 1
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
_: 1
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
_: 1
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
_: 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const k = /* @__PURE__ */ C(V, [["render", O]]), T = {
|
|
||||||
install(e) {
|
|
||||||
e.component("EliOlaMundo", k);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
export {
|
|
||||||
k as EliOlaMundo,
|
|
||||||
T as default
|
|
||||||
};
|
|
||||||
1
dist/eli-vue.umd.js
vendored
1
dist/eli-vue.umd.js
vendored
|
|
@ -1 +0,0 @@
|
||||||
(function(t,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue"),require("vuetify/components/VBtn"),require("vuetify/components/VCard"),require("vuetify/components/VGrid")):typeof define=="function"&&define.amd?define(["exports","vue","vuetify/components/VBtn","vuetify/components/VCard","vuetify/components/VGrid"],e):(t=typeof globalThis<"u"?globalThis:t||self,e(t.eli_vue={},t.Vue,t.VBtn,t.VCard,t.VGrid))})(this,(function(t,e,f,i,s){"use strict";const a=e.defineComponent({name:"EliOlaMundo"}),c=(o,n)=>{const d=o.__vccOpts||o;for(const[l,u]of n)d[l]=u;return d};function p(o,n,d,l,u,m){return e.openBlock(),e.createBlock(s.VContainer,null,{default:e.withCtx(()=>[e.createVNode(i.VCard,{class:"mx-auto",max_width:"400"},{default:e.withCtx(()=>[e.createVNode(i.VCardTitle,null,{default:e.withCtx(()=>[...n[0]||(n[0]=[e.createTextVNode("Olá Mundo!",-1)])]),_:1}),e.createVNode(i.VCardText,null,{default:e.withCtx(()=>[...n[1]||(n[1]=[e.createTextVNode(" Este é um componente de exemplo integrado com Vuetify. ",-1)])]),_:1}),e.createVNode(i.VCardActions,null,{default:e.withCtx(()=>[e.createVNode(f.VBtn,{color:"primary",block:""},{default:e.withCtx(()=>[...n[2]||(n[2]=[e.createTextVNode(" Botão Vuetify ",-1)])]),_:1})]),_:1})]),_:1})]),_:1})}const r=c(a,[["render",p]]),V={install(o){o.component("EliOlaMundo",r)}};t.EliOlaMundo=r,t.default=V,Object.defineProperties(t,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
||||||
declare const _default: typeof __VLS_export;
|
|
||||||
export default _default;
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import EliOlaMundo from './EliOlaMundo.vue';
|
|
||||||
export { EliOlaMundo };
|
|
||||||
export default EliOlaMundo;
|
|
||||||
7
dist/types/index.d.ts
vendored
7
dist/types/index.d.ts
vendored
|
|
@ -1,7 +0,0 @@
|
||||||
import type { App } from "vue";
|
|
||||||
import { EliOlaMundo } from './componentes/EliOlaMundo';
|
|
||||||
export { EliOlaMundo };
|
|
||||||
declare const _default: {
|
|
||||||
install(app: App): void;
|
|
||||||
};
|
|
||||||
export default _default;
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
import EliBotao from "./EliBotao.vue";
|
|
||||||
|
|
||||||
export { EliBotao };
|
|
||||||
export default EliBotao;
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container>
|
|
||||||
<v-card class="mx-auto" max_width="400">
|
|
||||||
<v-card-title>Olá Mundo!</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
Este é um componente de exemplo integrado com Vuetify.
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn color="primary" variant="elevated" block style="padding: 10px;">
|
|
||||||
Botão Vuetify
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from "vue"
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "EliOlaMundo",
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
# EliOlaMundo (Exemplo de Estrutura)
|
|
||||||
|
|
||||||
Este componente serve como **referência oficial** para a criação de novos componentes neste Design System. Utilize esta estrutura como guia.
|
|
||||||
|
|
||||||
## 📂 Estrutura de Pastas e Arquivos
|
|
||||||
|
|
||||||
Todo componente deve seguir rigorosamente a estrutura abaixo, utilizando **PascalCase** (PrimeiraLetraMaiuscula) para pastas e arquivos.
|
|
||||||
|
|
||||||
```
|
|
||||||
src/componentes/
|
|
||||||
└── NomeDoComponente/ <-- Pasta do Componente
|
|
||||||
├── NomeDoComponente.vue <-- Lógica e Template (Vue 3 + TS)
|
|
||||||
├── index.ts <-- Ponto de entrada (Exports)
|
|
||||||
└── README.md <-- Documentação de uso (Props, Slots, Exemplos)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Como Criar um Novo Componente
|
|
||||||
|
|
||||||
1. **Crie a Pasta:**
|
|
||||||
Nomeie a pasta com o nome do componente em PascalCase.
|
|
||||||
*Ex: `src/componentes/MeuBotao/`*
|
|
||||||
|
|
||||||
2. **Crie o Arquivo Vue:**
|
|
||||||
Nomeie o arquivo igual à pasta.
|
|
||||||
*Ex: `src/componentes/MeuBotao/MeuBotao.vue`*
|
|
||||||
- Utilize a Composition API com TypeScript.
|
|
||||||
- Defina sempre o `name` do componente.
|
|
||||||
|
|
||||||
3. **Crie o Index (`index.ts`):**
|
|
||||||
Este arquivo facilita a importação. Deve conter:
|
|
||||||
```typescript
|
|
||||||
import MeuBotao from './MeuBotao.vue'
|
|
||||||
|
|
||||||
export { MeuBotao }
|
|
||||||
export default MeuBotao
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Crie a Documentação (`README.md`):**
|
|
||||||
Descreva o que o componente faz, suas propriedades (`props`), eventos (`emits`) e slots.
|
|
||||||
|
|
||||||
5. **Registre na Biblioteca:**
|
|
||||||
Adicione o novo componente no arquivo principal `src/index.ts`:
|
|
||||||
```typescript
|
|
||||||
import { MeuBotao } from './componentes/MeuBotao'
|
|
||||||
// ...
|
|
||||||
export { MeuBotao }
|
|
||||||
|
|
||||||
export default {
|
|
||||||
install(app: App) {
|
|
||||||
// ...
|
|
||||||
app.component('MeuBotao', MeuBotao)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
import EliOlaMundo from './EliOlaMundo.vue'
|
|
||||||
|
|
||||||
export { EliOlaMundo }
|
|
||||||
export default EliOlaMundo
|
|
||||||
|
|
@ -14,19 +14,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType } from "vue";
|
import { defineComponent, PropType } from "vue";
|
||||||
|
import type { BotaoTamanho, BotaoVariante } from "../../tipos";
|
||||||
type BotaoVariant =
|
|
||||||
| "elevated"
|
|
||||||
| "flat"
|
|
||||||
| "outlined"
|
|
||||||
| "text"
|
|
||||||
| "tonal"
|
|
||||||
|
|
||||||
type BotaoSize =
|
|
||||||
| "x-small"
|
|
||||||
| "small"
|
|
||||||
| "default"
|
|
||||||
| "large"
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "EliBotao",
|
name: "EliBotao",
|
||||||
|
|
@ -40,12 +28,12 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
variant: {
|
variant: {
|
||||||
type: String as PropType<BotaoVariant>,
|
type: String as PropType<BotaoVariante>,
|
||||||
default: "elevated",
|
default: "elevated",
|
||||||
},
|
},
|
||||||
|
|
||||||
size: {
|
size: {
|
||||||
type: String as PropType<BotaoSize>,
|
type: String as PropType<BotaoTamanho>,
|
||||||
default: "default",
|
default: "default",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -61,4 +49,4 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -116,7 +116,7 @@ Exemplos:
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue'
|
import { defineComponent, ref } from 'vue'
|
||||||
import EliBotao from '@/components/EliBotao.vue'
|
import EliBotao from '@/componentes/botao/EliBotao.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { EliBotao },
|
components: { EliBotao },
|
||||||
1
src/componentes/botao/index.ts
Normal file
1
src/componentes/botao/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as EliBotao } from "./EliBotao.vue";
|
||||||
297
src/componentes/campo/EliInput.vue
Normal file
297
src/componentes/campo/EliInput.vue
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
<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"
|
||||||
|
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, 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 já 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",
|
||||||
|
"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.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 "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>
|
||||||
117
src/componentes/campo/README.md
Normal file
117
src/componentes/campo/README.md
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
# 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';
|
||||||
|
|
||||||
|
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).
|
||||||
|
* 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.
|
||||||
1
src/componentes/campo/index.ts
Normal file
1
src/componentes/campo/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as EliInput } from "./EliInput.vue";
|
||||||
9
src/componentes/campo/utils/cep.ts
Normal file
9
src/componentes/campo/utils/cep.ts
Normal file
|
|
@ -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");
|
||||||
|
}
|
||||||
24
src/componentes/campo/utils/cpfCnpj.ts
Normal file
24
src/componentes/campo/utils/cpfCnpj.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
function somenteNumeros(v: string): string {
|
||||||
|
return v.replace(/\D+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatarCpfCnpj(v: string): string {
|
||||||
|
const d = somenteNumeros(v);
|
||||||
|
|
||||||
|
// CPF
|
||||||
|
if (d.length <= 11) {
|
||||||
|
return d
|
||||||
|
.replace(/(\d{3})(\d)/, "$1.$2")
|
||||||
|
.replace(/(\d{3})(\d)/, "$1.$2")
|
||||||
|
.replace(/(\d{3})(\d{1,2})$/, "$1-$2")
|
||||||
|
.slice(0, 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CNPJ
|
||||||
|
return d
|
||||||
|
.replace(/^(\d{2})(\d)/, "$1.$2")
|
||||||
|
.replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3")
|
||||||
|
.replace(/\.(\d{3})(\d)/, ".$1/$2")
|
||||||
|
.replace(/(\d{4})(\d)/, "$1-$2")
|
||||||
|
.slice(0, 18);
|
||||||
|
}
|
||||||
17
src/componentes/campo/utils/numerico.ts
Normal file
17
src/componentes/campo/utils/numerico.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, ".");
|
||||||
|
}
|
||||||
31
src/componentes/campo/utils/telefone.ts
Normal file
31
src/componentes/campo/utils/telefone.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
// utils/telefone.ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove tudo que não é número
|
||||||
|
*/
|
||||||
|
export function sanitizeTelefone(value: string): string {
|
||||||
|
return value.replace(/\D+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aplica máscara dinâmica de telefone BR
|
||||||
|
*/
|
||||||
|
export function formatTelefone(value: string): string {
|
||||||
|
const digits = sanitizeTelefone(value);
|
||||||
|
|
||||||
|
if (!digits) return "";
|
||||||
|
|
||||||
|
// (99) 9999-9999
|
||||||
|
if (digits.length <= 10) {
|
||||||
|
return digits
|
||||||
|
.replace(/^(\d{2})(\d)/, "($1) $2")
|
||||||
|
.replace(/(\d{4})(\d)/, "$1-$2")
|
||||||
|
.slice(0, 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (99) 99999-9999
|
||||||
|
return digits
|
||||||
|
.replace(/^(\d{2})(\d)/, "($1) $2")
|
||||||
|
.replace(/(\d{5})(\d)/, "$1-$2")
|
||||||
|
.slice(0, 15);
|
||||||
|
}
|
||||||
148
src/componentes/cartao/EliCartao.vue
Normal file
148
src/componentes/cartao/EliCartao.vue
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
<template>
|
||||||
|
<v-card
|
||||||
|
class="eli-cartao"
|
||||||
|
:variant="variant"
|
||||||
|
:class="classeStatus"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<v-card-title class="eli-cartao__titulo">
|
||||||
|
<div class="eli-cartao__titulo-texto">
|
||||||
|
<slot name="titulo">
|
||||||
|
{{ titulo }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Indicador de status (badge) padronizado pelo design system -->
|
||||||
|
<div class="eli-cartao__status">
|
||||||
|
<EliBadge
|
||||||
|
:badge="rotuloStatus"
|
||||||
|
radius="pill"
|
||||||
|
:color="corStatus"
|
||||||
|
>
|
||||||
|
<span />
|
||||||
|
</EliBadge>
|
||||||
|
</div>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="eli-cartao__conteudo">
|
||||||
|
<slot />
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions v-if="$slots.acoes" class="eli-cartao__acoes">
|
||||||
|
<slot name="acoes" />
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType } from "vue";
|
||||||
|
import type { CartaoStatus } from "../../tipos";
|
||||||
|
import { EliBadge } from "../indicador";
|
||||||
|
|
||||||
|
type CartaoVariante = "outlined" | "flat" | "elevated" | "tonal";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EliCartao
|
||||||
|
*
|
||||||
|
* Um cartão de domínio para listas/pipelines (ex.: oportunidades/propostas) com:
|
||||||
|
* - título
|
||||||
|
* - status padronizado (novo/rascunho/vendido/cancelado)
|
||||||
|
* - slot padrão para conteúdo
|
||||||
|
* - slot opcional para ações
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: "EliCartao",
|
||||||
|
components: { EliBadge },
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
/** Título de fallback caso o slot `titulo` não seja usado. */
|
||||||
|
titulo: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status semântico do cartão.
|
||||||
|
* Usado para cor/label e para permitir filtros por status.
|
||||||
|
*/
|
||||||
|
status: {
|
||||||
|
type: String as PropType<CartaoStatus>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Variante visual do v-card (Vuetify). */
|
||||||
|
variant: {
|
||||||
|
type: String as PropType<CartaoVariante>,
|
||||||
|
default: "outlined",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
/** Emit opcional para padronizar clique no cartão. */
|
||||||
|
clicar: (_status: CartaoStatus) => true,
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const rotuloStatus = computed(() => {
|
||||||
|
// Mantém label em PT-BR (não abreviar)
|
||||||
|
return props.status;
|
||||||
|
});
|
||||||
|
|
||||||
|
const corStatus = computed(() => {
|
||||||
|
// Cor neutra por padrão e semântica por status
|
||||||
|
switch (props.status) {
|
||||||
|
case "novo":
|
||||||
|
return "primary";
|
||||||
|
case "rascunho":
|
||||||
|
return "secondary";
|
||||||
|
case "vendido":
|
||||||
|
return "success";
|
||||||
|
case "cancelado":
|
||||||
|
return "error";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const classeStatus = computed(() => `eli-cartao--${props.status}`);
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
emit("clicar", props.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rotuloStatus,
|
||||||
|
corStatus,
|
||||||
|
classeStatus,
|
||||||
|
onClick,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.eli-cartao {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eli-cartao__titulo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eli-cartao__titulo-texto {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eli-cartao__conteudo {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eli-cartao__acoes {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hooks visuais por status (sem forçar cor, deixa o tema do Vuetify fazer o trabalho) */
|
||||||
|
.eli-cartao--cancelado {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
71
src/componentes/cartao/README.md
Normal file
71
src/componentes/cartao/README.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# EliCartao
|
||||||
|
|
||||||
|
O `EliCartao` é um componente de cartão focado em **listas/pipelines** (ex.: oportunidades e propostas comerciais), com um **status semântico** padronizado.
|
||||||
|
|
||||||
|
Ele encapsula o `v-card` (Vuetify) e adiciona:
|
||||||
|
|
||||||
|
- Área de **título**
|
||||||
|
- **Badge de status** padronizado (`novo`, `rascunho`, `vendido`, `cancelado`)
|
||||||
|
- Slot de **conteúdo**
|
||||||
|
- Slot opcional de **ações**
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| Prop | Tipo | Padrão | Descrição |
|
||||||
|
|----------|--------------------------------------------------|--------------|-----------|
|
||||||
|
| `titulo` | `string` | `""` | Título de fallback (se o slot `titulo` não for usado). |
|
||||||
|
| `status` | `"novo" \| "rascunho" \| "vendido" \| "cancelado"` | *(obrigatório)* | Status semântico do cartão. Controla label/cor do badge e classes. |
|
||||||
|
| `variant`| `"outlined" \| "flat" \| "elevated" \| "tonal"` | `"outlined"` | Variante do `v-card` (Vuetify). |
|
||||||
|
|
||||||
|
### Emits
|
||||||
|
|
||||||
|
| Evento | Payload | Quando dispara |
|
||||||
|
|----------|-------------------------|----------------|
|
||||||
|
| `clicar` | `(status: CartaoStatus)`| Quando o cartão for clicado (opcional; útil para padronizar navegação). |
|
||||||
|
|
||||||
|
> Observação: o componente deixa `v-bind="$attrs"` para você passar `class`, `style`, `to`, `href`, etc.
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
| Slot | Objetivo |
|
||||||
|
|----------|----------|
|
||||||
|
| `titulo` | Conteúdo customizado do título (override do `titulo`). |
|
||||||
|
| `default`| Corpo do cartão. |
|
||||||
|
| `acoes` | Rodapé de ações (botões/links). |
|
||||||
|
|
||||||
|
## Exemplos
|
||||||
|
|
||||||
|
### 1) Cartão simples
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<EliCartao titulo="Proposta #1024" status="novo">
|
||||||
|
<div><strong>Cliente:</strong> ACME Ltda</div>
|
||||||
|
<div><strong>Valor:</strong> R$ 12.500,00</div>
|
||||||
|
</EliCartao>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) Título via slot + ações
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<EliCartao status="vendido">
|
||||||
|
<template #titulo>
|
||||||
|
<span>Oportunidade #204</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div>Cliente: Empresa X</div>
|
||||||
|
|
||||||
|
<template #acoes>
|
||||||
|
<EliBotao variant="text">Abrir</EliBotao>
|
||||||
|
<EliBotao variant="text" color="secondary">Editar</EliBotao>
|
||||||
|
</template>
|
||||||
|
</EliCartao>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Casos de borda / comportamento esperado
|
||||||
|
|
||||||
|
- `status` é obrigatório e sempre deve ser um valor suportado.
|
||||||
|
- Status `cancelado` aplica leve redução de opacidade via classe CSS.
|
||||||
|
- A renderização de ações só ocorre se o slot `acoes` existir.
|
||||||
|
|
||||||
2
src/componentes/cartao/index.ts
Normal file
2
src/componentes/cartao/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as EliCartao } from "./EliCartao.vue";
|
||||||
|
|
||||||
118
src/componentes/indicador/EliBadge.vue
Normal file
118
src/componentes/indicador/EliBadge.vue
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
<template>
|
||||||
|
<v-badge
|
||||||
|
v-if="showBadge"
|
||||||
|
:color="color"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:location="location"
|
||||||
|
:offset-x="offsetX"
|
||||||
|
:offset-y="offsetY"
|
||||||
|
:dot="dot"
|
||||||
|
:content="badge"
|
||||||
|
:style="badgeStyle"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</v-badge>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType } from "vue";
|
||||||
|
import type {
|
||||||
|
CssLength,
|
||||||
|
IndicadorLocalizacao,
|
||||||
|
IndicadorOffset,
|
||||||
|
IndicadorPresetRaio,
|
||||||
|
} from "../../tipos";
|
||||||
|
|
||||||
|
const RADIUS_MAP: Record<IndicadorPresetRaio, string> = {
|
||||||
|
suave: "4px",
|
||||||
|
pill: "10px",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "EliBadge",
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: "primary",
|
||||||
|
},
|
||||||
|
|
||||||
|
location: {
|
||||||
|
type: String as PropType<IndicadorLocalizacao>,
|
||||||
|
default: "top right",
|
||||||
|
},
|
||||||
|
|
||||||
|
offsetX: {
|
||||||
|
type: String as PropType<IndicadorOffset>,
|
||||||
|
default: "0",
|
||||||
|
},
|
||||||
|
|
||||||
|
offsetY: {
|
||||||
|
type: String as PropType<IndicadorOffset>,
|
||||||
|
default: "0",
|
||||||
|
},
|
||||||
|
|
||||||
|
dot: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
badge: {
|
||||||
|
type: [String, Number] as PropType<string | number | undefined>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 🔥 NOVO: controla só o radius */
|
||||||
|
radius: {
|
||||||
|
type: String as PropType<IndicadorPresetRaio | CssLength>,
|
||||||
|
default: "suave",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
|
||||||
|
const resolvedRadius = computed(() => {
|
||||||
|
// preset conhecido
|
||||||
|
if (props.radius in RADIUS_MAP) {
|
||||||
|
return RADIUS_MAP[props.radius as IndicadorPresetRaio];
|
||||||
|
}
|
||||||
|
|
||||||
|
// valor custom (ex: "8px", "50%", "0")
|
||||||
|
return props.radius;
|
||||||
|
});
|
||||||
|
const showBadge = computed(() => {
|
||||||
|
// se for dot, respeita visible
|
||||||
|
if (props.dot) return props.visible;
|
||||||
|
|
||||||
|
// se tiver badge, respeita visible
|
||||||
|
if (props.badge !== undefined) return props.visible;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const badgeStyle = computed(() => ({
|
||||||
|
"--eli-badge-radius": resolvedRadius.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { showBadge, badgeStyle };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.v-badge__badge),
|
||||||
|
:deep(.v-badge__content) {
|
||||||
|
border-radius: var(--eli-badge-radius) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
182
src/componentes/indicador/README.md
Normal file
182
src/componentes/indicador/README.md
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
# EliBadge
|
||||||
|
|
||||||
|
**Componente base de badge do design system**
|
||||||
|
|
||||||
|
O `EliBadge` encapsula o `v-badge` do Vuetify para aplicar padrões visuais, tipagem e comportamento previsível em toda a aplicação.
|
||||||
|
|
||||||
|
> ⚠️ **Nunca use `v-badge` diretamente fora do design system.**
|
||||||
|
> Utilize sempre o `EliBadge` para manter consistência visual e compatibilidade entre versões do Vuetify.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Visão geral
|
||||||
|
|
||||||
|
O `EliBadge` foi projetado para:
|
||||||
|
|
||||||
|
- Garantir consistência visual no uso de badges;
|
||||||
|
- Fornecer uma API tipada (TypeScript) e com autocomplete;
|
||||||
|
- Controlar corretamente a exibição do badge sem afetar o conteúdo principal;
|
||||||
|
- Permitir presets semânticos de `border-radius` e valores CSS customizados;
|
||||||
|
- Repassar atributos e listeners de forma transparente ao `v-badge`.
|
||||||
|
|
||||||
|
### Principais decisões de implementação
|
||||||
|
|
||||||
|
- `inheritAttrs: false` — o componente faz `v-bind="$attrs"` manualmente no `v-badge`.
|
||||||
|
- Uso de CSS Variable (`--eli-badge-radius`) para controlar o `border-radius`.
|
||||||
|
- Presets tipados para `radius` + fallback para valores CSS livres.
|
||||||
|
- O slot padrão **nunca é removido**: apenas o badge é condicional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tipagem (TypeScript)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type LocalBadge =
|
||||||
|
| "top right"
|
||||||
|
| "right center"
|
||||||
|
| "bottom right"
|
||||||
|
| "top center"
|
||||||
|
| "bottom center"
|
||||||
|
| "top left"
|
||||||
|
| "left center"
|
||||||
|
| "bottom left";
|
||||||
|
|
||||||
|
type Offset = "-20" | "-15" | "-10" | "-5" | "0" | "20" | "15" | "10" | "5";
|
||||||
|
|
||||||
|
type BadgeRadiusPreset = "suave" | "pill";
|
||||||
|
|
||||||
|
type CssLength = `${number}px` | `${number}rem` | `${number}%` | "0";
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Props
|
||||||
|
| Prop | Tipo | Default | Descrição |
|
||||||
|
| ---------- | -------------------------------- | ------------- | ---------------------------------------------- |
|
||||||
|
| `color` | `string` | `"primary"` | Cor visual do badge (Vuetify theme). |
|
||||||
|
| `location` | `LocalBadge` | `"top right"` | Posição do badge em relação ao conteúdo. |
|
||||||
|
| `offsetX` | `Offset` | `"0"` | Deslocamento horizontal. |
|
||||||
|
| `offsetY` | `Offset` | `"0"` | Deslocamento vertical. |
|
||||||
|
| `dot` | `boolean` | `false` | Exibe badge no formato de ponto. |
|
||||||
|
| `visible` | `boolean` | `true` | Controla a exibição do badge (slot permanece). |
|
||||||
|
| `badge` | `string \| number \| undefined` | `undefined` | Conteúdo textual ou numérico do badge. |
|
||||||
|
| `radius` | `BadgeRadiusPreset \| CssLength` | `"suave"` | Preset ou valor CSS de `border-radius`. |
|
||||||
|
|
||||||
|
Presets de radius
|
||||||
|
{
|
||||||
|
suave: "4px", // cantos levemente arredondados
|
||||||
|
pill: "10px", // cantos bem arredondados
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Repasso de atributos e listeners
|
||||||
|
|
||||||
|
Como inheritAttrs: false e v-bind="$attrs" são usados:
|
||||||
|
|
||||||
|
1. Atributos HTML (ex.: type, aria-label, class, style) passados para <EliBadge> serão aplicados ao v-badge filho.
|
||||||
|
|
||||||
|
2. Listeners (ex.: @click) também são repassados ao v-badge — use o componente como se estivesse escutando eventos diretamente no v-badge.
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
<EliBadge badge="3" aria-label="Notificações">
|
||||||
|
<v-icon>mdi-bell</v-icon>
|
||||||
|
</EliBadge>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Slot
|
||||||
|
|
||||||
|
O EliBadge expõe um slot padrão para o conteúdo que será "badged" — normalmente um ícone, avatar ou texto.
|
||||||
|
|
||||||
|
Exemplos:
|
||||||
|
|
||||||
|
<EliBadge badge="3">
|
||||||
|
<v-icon>mdi-bell</v-icon>
|
||||||
|
</EliBadge>
|
||||||
|
|
||||||
|
<EliBadge badge="Novo">
|
||||||
|
<button>Vistoria</button>
|
||||||
|
</EliBadge>
|
||||||
|
|
||||||
|
Se visible for false, o slot continua sendo renderizado (o badge some, mas o conteúdo permanece).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Exemplos de uso
|
||||||
|
|
||||||
|
Preset suave (padrão):
|
||||||
|
<EliBadge badge="5" radius="suave">
|
||||||
|
<v-icon>mdi-email</v-icon>
|
||||||
|
</EliBadge>
|
||||||
|
|
||||||
|
Preset pill (mais arredondado):
|
||||||
|
<EliBadge badge="99+" radius="pill">
|
||||||
|
<v-icon>mdi-chat</v-icon>
|
||||||
|
</EliBadge>
|
||||||
|
|
||||||
|
Valor custom:
|
||||||
|
<EliBadge badge="1" radius="0"> <!-- totalmente reto -->
|
||||||
|
<v-icon>mdi-alert</v-icon>
|
||||||
|
</EliBadge>
|
||||||
|
|
||||||
|
<EliBadge badge="8" radius="12px">
|
||||||
|
<v-icon>mdi-star</v-icon>
|
||||||
|
</EliBadge>
|
||||||
|
|
||||||
|
|
||||||
|
Esconder só o badge (manter conteúdo):
|
||||||
|
<EliBadge badge="Novo" :visible="false">
|
||||||
|
Vistoria
|
||||||
|
</EliBadge>
|
||||||
|
<!-- RENDERIZA: "Vistoria" (sem o indicador "Novo") -->
|
||||||
|
|
||||||
|
Mostrar dot (ponto):
|
||||||
|
<EliBadge dot :visible="true">
|
||||||
|
<img src="avatar.png" alt="Usuário"/>
|
||||||
|
</EliBadge>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Acessibilidade (A11y)
|
||||||
|
|
||||||
|
1. Forneça aria-label quando o badge transmitir informação importante sem texto adicional.
|
||||||
|
2. Evite usar cor sozinha para transmitir significado — combine com texto ou atributos ARIA.
|
||||||
|
3. Para badges que comunicam contagem (ex.: notificações), adicione aria-live ou texto alternativo
|
||||||
|
no componente pai conforme a necessidade do caso de uso.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Boas práticas
|
||||||
|
|
||||||
|
1. Prefira presets (suave, pill) para consistência visual; use valores custom apenas quando necessário.
|
||||||
|
2. Não aplique estilos inline que conflitem com tokens do design system; prefira classes e variáveis do Vuetify.
|
||||||
|
3. Documente o uso do visible e badge nos locais onde o componente for amplamente adotado.
|
||||||
|
4. Evite usar visible=false se você espera apenas esconder zero/empty — prefira lógica que passe badge = undefined ou :visible="count > 0".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Testes
|
||||||
|
|
||||||
|
Recomenda-se testar:
|
||||||
|
|
||||||
|
1. Renderização com badge presente e badge === undefined.
|
||||||
|
2. Comportamento de visible (assegurar que o slot continua visível quando visible=false).
|
||||||
|
3. dot true/false.
|
||||||
|
4. Aplicação da variável CSS (--eli-badge-radius) e que o border-radius interno do Vuetify muda conforme o radius.
|
||||||
|
5. $attrs repassados para o v-badge (por exemplo: aria-label, class).
|
||||||
|
|
||||||
|
Exemplo (pseudocódigo):
|
||||||
|
const wrapper = mount(EliBadge, {
|
||||||
|
props: { badge: '3' },
|
||||||
|
slots: { default: '<button>Inbox</button>' }
|
||||||
|
});
|
||||||
|
expect(wrapper.html()).toContain('Inbox');
|
||||||
|
expect(wrapper.findComponent({ name: 'v-badge' }).exists()).toBe(true);
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Observações sobre Vuetify
|
||||||
|
|
||||||
|
1. O EliBadge usa seletor com `:deep(...)` para alterar o border-radius do elemento interno do `v-badge`. Se você atualizar o Vuetify, verifique os nomes de classe (`.v-badge__badge` / `.v-badge__content`) e ajuste o seletor se necessário.
|
||||||
|
|
||||||
|
2. Prop names do v-badge (ex.: location, offset-x, offset-y, content, dot) podem variar entre versões do Vuetify — reveja a docs da versão em uso se algo não for aplicado como esperado.
|
||||||
1
src/componentes/indicador/index.ts
Normal file
1
src/componentes/indicador/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as EliBadge } from "./EliBadge.vue";
|
||||||
166
src/componentes/ola_mundo/EliOlaMundo.vue
Normal file
166
src/componentes/ola_mundo/EliOlaMundo.vue
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-card class="mx-auto" max_width="400">
|
||||||
|
<v-card-title>
|
||||||
|
<EliBadge :badge="'Novo'" offset-x="-15" location="right center">
|
||||||
|
Olá Mundo!
|
||||||
|
</EliBadge>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
Este é um componente de exemplo integrado com Vuetify.
|
||||||
|
|
||||||
|
<div class="grid-example">
|
||||||
|
<!-- text normal -->
|
||||||
|
<EliInput
|
||||||
|
v-model="nome"
|
||||||
|
label="Nome"
|
||||||
|
placeholder="Digite o nome"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EliInput
|
||||||
|
v-model="idade"
|
||||||
|
type="numericoInteiro"
|
||||||
|
label="Idade"
|
||||||
|
density="default"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EliInput
|
||||||
|
v-model="altura"
|
||||||
|
type="numericoDecimal"
|
||||||
|
label="Altura"
|
||||||
|
density="comfortable"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EliInput v-model="valor" type="numericoMoeda" label="Valor" />
|
||||||
|
|
||||||
|
<EliInput v-model="telefone" type="telefone" label="Telefone" />
|
||||||
|
|
||||||
|
<EliInput
|
||||||
|
v-model="cep"
|
||||||
|
type="cep"
|
||||||
|
label="CEP"
|
||||||
|
placeholder="00000-000"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EliInput
|
||||||
|
type="select"
|
||||||
|
label="Estado"
|
||||||
|
: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>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<EliBotao color="primary" variant="elevated" block>
|
||||||
|
Botão Vuetify
|
||||||
|
</EliBotao>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from "vue";
|
||||||
|
import EliBotao from "../botao/EliBotao.vue";
|
||||||
|
import EliBadge from "../indicador/EliBadge.vue";
|
||||||
|
import EliInput from "../campo/EliInput.vue";
|
||||||
|
|
||||||
|
type Habilidade = "vue" | "react";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "EliOlaMundo",
|
||||||
|
components: {
|
||||||
|
EliBotao,
|
||||||
|
EliBadge,
|
||||||
|
EliInput,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const nome = ref("");
|
||||||
|
const estado = ref<string[]>([]);
|
||||||
|
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<Habilidade[]>([]);
|
||||||
|
return {
|
||||||
|
nome,
|
||||||
|
email,
|
||||||
|
documento,
|
||||||
|
estado,
|
||||||
|
telefone,
|
||||||
|
mensagem,
|
||||||
|
senha,
|
||||||
|
cor,
|
||||||
|
habilidades,
|
||||||
|
idade,
|
||||||
|
altura,
|
||||||
|
cep,
|
||||||
|
valor,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
34
src/componentes/ola_mundo/README.md
Normal file
34
src/componentes/ola_mundo/README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# EliOlaMundo (componente de demonstração)
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## Estrutura do repositório (padrão)
|
||||||
|
|
||||||
|
Neste Design System:
|
||||||
|
|
||||||
|
- **Pastas e arquivos** (quando aplicável) preferem português: `botao/`, `campo/`, `indicador/`, etc.
|
||||||
|
- **Componentes** mantêm prefixo técnico `Eli` (PascalCase): `EliBotao`, `EliInput`.
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/componentes/
|
||||||
|
botao/
|
||||||
|
EliBotao.vue
|
||||||
|
index.ts
|
||||||
|
README.md
|
||||||
|
campo/
|
||||||
|
EliInput.vue
|
||||||
|
index.ts
|
||||||
|
README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Playground
|
||||||
|
|
||||||
|
O playground deve conter arquivos `*.playground.vue` para cada componente, com no mínimo:
|
||||||
|
|
||||||
|
- 3 variações visuais/estados
|
||||||
|
- interações (v-model/click)
|
||||||
|
- casos de borda relevantes
|
||||||
1
src/componentes/ola_mundo/index.ts
Normal file
1
src/componentes/ola_mundo/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as EliOlaMundo } from "./EliOlaMundo.vue";
|
||||||
19
src/index.ts
19
src/index.ts
|
|
@ -1,13 +1,24 @@
|
||||||
import type { App } from "vue";
|
import type { App, Plugin } from "vue";
|
||||||
import { EliOlaMundo } from "./componentes/EliOlaMundo";
|
import { EliOlaMundo } from "./componentes/ola_mundo";
|
||||||
import { EliBotao } from "./componentes/EliBotao";
|
import { EliBotao } from "./componentes/botao";
|
||||||
|
import { EliBadge } from "./componentes/indicador";
|
||||||
|
import { EliInput } from "./componentes/campo";
|
||||||
|
import { EliCartao } from "./componentes/cartao";
|
||||||
|
|
||||||
export { EliOlaMundo };
|
export { EliOlaMundo };
|
||||||
export { EliBotao };
|
export { EliBotao };
|
||||||
|
export { EliBadge };
|
||||||
|
export { EliInput };
|
||||||
|
export { EliCartao };
|
||||||
|
|
||||||
export default {
|
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("EliInput", EliInput);
|
||||||
|
app.component("EliCartao", EliCartao);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default EliVue;
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,46 @@
|
||||||
<template>
|
<template>
|
||||||
<EliOlaMundo />
|
<v-container class="py-6">
|
||||||
<EliBotao
|
<h1 class="text-h5 mb-4">Playground — eli-vue</h1>
|
||||||
color="primary"
|
|
||||||
@click="() => {console.log('xxx')}"
|
<v-tabs v-model="aba" color="primary" density="comfortable">
|
||||||
>
|
<v-tab value="botao">Botão</v-tab>
|
||||||
Button
|
<v-tab value="indicador">Indicador</v-tab>
|
||||||
</EliBotao>
|
<v-tab value="cartao">Cartão</v-tab>
|
||||||
|
<v-tab value="campo">Campo</v-tab>
|
||||||
|
<v-tab value="ola_mundo">Demo</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
|
||||||
|
<v-divider class="my-4" />
|
||||||
|
|
||||||
|
<BotaoPlayground v-if="aba === 'botao'" />
|
||||||
|
<IndicadorPlayground v-else-if="aba === 'indicador'" />
|
||||||
|
<CartaoPlayground v-else-if="aba === 'cartao'" />
|
||||||
|
<CampoPlayground v-else-if="aba === 'campo'" />
|
||||||
|
<OlaMundoPlayground v-else />
|
||||||
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import { EliOlaMundo } from '@/componentes/EliOlaMundo'
|
import BotaoPlayground from "./botao.playground.vue";
|
||||||
import {EliBotao} from '@/componentes/EliBotao';
|
import IndicadorPlayground from "./indicador.playground.vue";
|
||||||
|
import CartaoPlayground from "./cartao.playground.vue";
|
||||||
|
import CampoPlayground from "./campo.playground.vue";
|
||||||
|
import OlaMundoPlayground from "./ola_mundo.playground.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
EliOlaMundo,
|
BotaoPlayground,
|
||||||
EliBotao,
|
IndicadorPlayground,
|
||||||
|
CartaoPlayground,
|
||||||
|
CampoPlayground,
|
||||||
|
OlaMundoPlayground,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
aba: "botao" as "botao" | "indicador" | "cartao" | "campo" | "ola_mundo",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
45
src/playground/botao.playground.vue
Normal file
45
src/playground/botao.playground.vue
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<section class="stack">
|
||||||
|
<h2>EliBotao</h2>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<EliBotao @click="contador++">Primário (clicks: {{ contador }})</EliBotao>
|
||||||
|
<EliBotao variant="outlined">Outlined</EliBotao>
|
||||||
|
<EliBotao variant="text">Text</EliBotao>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<EliBotao :disabled="true">Desabilitado</EliBotao>
|
||||||
|
<EliBotao :loading="true">Carregando</EliBotao>
|
||||||
|
<EliBotao color="success">Success</EliBotao>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from "vue";
|
||||||
|
import { EliBotao } from "@/componentes/botao";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "BotaoPlayground",
|
||||||
|
components: { EliBotao },
|
||||||
|
setup() {
|
||||||
|
const contador = ref(0);
|
||||||
|
return { contador };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
123
src/playground/campo.playground.vue
Normal file
123
src/playground/campo.playground.vue
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
<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="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 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,
|
||||||
|
cep: cep.value,
|
||||||
|
estado: estado.value,
|
||||||
|
cor: cor.value,
|
||||||
|
habilidades: habilidades.value,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
nome,
|
||||||
|
telefone,
|
||||||
|
documento,
|
||||||
|
idade,
|
||||||
|
valor,
|
||||||
|
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>
|
||||||
202
src/playground/cartao.playground.vue
Normal file
202
src/playground/cartao.playground.vue
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
<template>
|
||||||
|
<section class="stack">
|
||||||
|
<h2>EliCartao (pipeline)</h2>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<EliInput
|
||||||
|
v-model="filtro"
|
||||||
|
label="Buscar"
|
||||||
|
placeholder="Cliente, título, valor..."
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EliBotao @click="criar">Novo card</EliBotao>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="colunas">
|
||||||
|
<section v-for="coluna in colunas" :key="coluna.status" class="coluna">
|
||||||
|
<header class="coluna-header">
|
||||||
|
<strong class="text-subtitle-1">{{ coluna.titulo }}</strong>
|
||||||
|
<EliBadge :badge="coluna.itens.length" radius="pill" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<EliCartao
|
||||||
|
v-for="item in itensFiltrados(coluna.itens)"
|
||||||
|
:key="item.id"
|
||||||
|
:titulo="item.titulo"
|
||||||
|
:status="coluna.status"
|
||||||
|
>
|
||||||
|
<div class="linha"><strong>Cliente:</strong> {{ item.cliente }}</div>
|
||||||
|
<div class="linha"><strong>Valor:</strong> {{ item.valor }}</div>
|
||||||
|
<div class="linha"><strong>Vencimento:</strong> {{ item.vencimento }}</div>
|
||||||
|
|
||||||
|
<template #acoes>
|
||||||
|
<EliBotao variant="text" @click="abrir(item)">Abrir</EliBotao>
|
||||||
|
<EliBotao variant="text" color="secondary" @click="editar(item)">
|
||||||
|
Editar
|
||||||
|
</EliBotao>
|
||||||
|
</template>
|
||||||
|
</EliCartao>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from "vue";
|
||||||
|
import { EliBadge } from "@/componentes/indicador";
|
||||||
|
import { EliBotao } from "@/componentes/botao";
|
||||||
|
import { EliInput } from "@/componentes/campo";
|
||||||
|
import { EliCartao } from "@/componentes/cartao";
|
||||||
|
import type { CartaoStatus } from "@/tipos";
|
||||||
|
|
||||||
|
type Card = {
|
||||||
|
id: string;
|
||||||
|
titulo: string;
|
||||||
|
cliente: string;
|
||||||
|
valor: string;
|
||||||
|
vencimento: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Coluna = {
|
||||||
|
status: CartaoStatus;
|
||||||
|
titulo: string;
|
||||||
|
itens: Card[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "CartaoPlayground",
|
||||||
|
components: { EliBadge, EliBotao, EliInput, EliCartao },
|
||||||
|
setup() {
|
||||||
|
const filtro = ref("");
|
||||||
|
|
||||||
|
const colunas = ref<Coluna[]>([
|
||||||
|
{
|
||||||
|
status: "novo",
|
||||||
|
titulo: "Novo",
|
||||||
|
itens: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
titulo: "Proposta #1024",
|
||||||
|
cliente: "ACME Ltda",
|
||||||
|
valor: "R$ 12.500,00",
|
||||||
|
vencimento: "10/01/2026",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: "rascunho",
|
||||||
|
titulo: "Rascunho",
|
||||||
|
itens: [
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
titulo: "Oportunidade #204",
|
||||||
|
cliente: "Empresa X",
|
||||||
|
valor: "R$ 8.000,00",
|
||||||
|
vencimento: "15/01/2026",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: "vendido",
|
||||||
|
titulo: "Vendido",
|
||||||
|
itens: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: "cancelado",
|
||||||
|
titulo: "Cancelado",
|
||||||
|
itens: [
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
titulo: "Proposta #999",
|
||||||
|
cliente: "Cliente Y",
|
||||||
|
valor: "R$ 3.200,00",
|
||||||
|
vencimento: "02/01/2026",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
function itensFiltrados(itens: Card[]) {
|
||||||
|
const q = filtro.value.trim().toLowerCase();
|
||||||
|
if (!q) return itens;
|
||||||
|
return itens.filter((i) =>
|
||||||
|
`${i.titulo} ${i.cliente} ${i.valor}`.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function criar() {
|
||||||
|
// Exemplo: criar card no status "novo"
|
||||||
|
colunas.value[0]?.itens.unshift({
|
||||||
|
id: String(Date.now()),
|
||||||
|
titulo: "Nova oportunidade",
|
||||||
|
cliente: "(definir)",
|
||||||
|
valor: "R$ 0,00",
|
||||||
|
vencimento: "--/--/----",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function abrir(_item: Card) {
|
||||||
|
// Navegar para detalhes
|
||||||
|
}
|
||||||
|
|
||||||
|
function editar(_item: Card) {
|
||||||
|
// Abrir modal/rota de edição
|
||||||
|
}
|
||||||
|
|
||||||
|
return { filtro, colunas, itensFiltrados, criar, abrir, editar };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
grid-template-columns: minmax(260px, 420px) auto;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colunas {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.coluna {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coluna-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linha {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.toolbar {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
65
src/playground/indicador.playground.vue
Normal file
65
src/playground/indicador.playground.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<section class="stack">
|
||||||
|
<h2>EliBadge (indicador)</h2>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<EliBadge badge="3">
|
||||||
|
<v-icon>mdi-bell</v-icon>
|
||||||
|
</EliBadge>
|
||||||
|
|
||||||
|
<EliBadge badge="Novo" radius="pill" color="success">
|
||||||
|
<span class="chip">Mensagens</span>
|
||||||
|
</EliBadge>
|
||||||
|
|
||||||
|
<EliBadge dot :visible="true" color="error">
|
||||||
|
<v-icon>mdi-email</v-icon>
|
||||||
|
</EliBadge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<EliBadge badge="99+" :visible="mostrar" radius="12px">
|
||||||
|
<span class="chip">Toggle visible</span>
|
||||||
|
</EliBadge>
|
||||||
|
|
||||||
|
<EliBotao variant="outlined" @click="mostrar = !mostrar">
|
||||||
|
Alternar visible ({{ mostrar ? "mostrando" : "oculto" }})
|
||||||
|
</EliBotao>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from "vue";
|
||||||
|
import { EliBadge } from "@/componentes/indicador";
|
||||||
|
import { EliBotao } from "@/componentes/botao";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "IndicadorPlayground",
|
||||||
|
components: { EliBadge, EliBotao },
|
||||||
|
setup() {
|
||||||
|
const mostrar = ref(true);
|
||||||
|
return { mostrar };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
34
src/playground/ola_mundo.playground.vue
Normal file
34
src/playground/ola_mundo.playground.vue
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<section class="stack">
|
||||||
|
<h2>EliOlaMundo</h2>
|
||||||
|
<p class="muted">
|
||||||
|
Demo integrada (útil para smoke-test visual). Para testes específicos,
|
||||||
|
prefira os playgrounds de <code>EliBotao</code>, <code>EliInput</code> e
|
||||||
|
<code>EliBadge</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<EliOlaMundo />
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { EliOlaMundo } from "@/componentes/ola_mundo";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "OlaMundoPlayground",
|
||||||
|
components: { EliOlaMundo },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
opacity: 0.75;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
src/tipos/botao.ts
Normal file
9
src/tipos/botao.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Tipos do componente EliBotao.
|
||||||
|
* Mantidos separados do componente para facilitar reuso e padronização.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type BotaoVariante = "elevated" | "flat" | "outlined" | "text" | "tonal";
|
||||||
|
|
||||||
|
export type BotaoTamanho = "x-small" | "small" | "default" | "large";
|
||||||
|
|
||||||
52
src/tipos/campo.ts
Normal file
52
src/tipos/campo.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* 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";
|
||||||
|
|
||||||
|
export type CampoTipo =
|
||||||
|
| "text"
|
||||||
|
| "password"
|
||||||
|
| "email"
|
||||||
|
| "search"
|
||||||
|
| "url"
|
||||||
|
| "textarea"
|
||||||
|
| "radio"
|
||||||
|
| "checkbox"
|
||||||
|
| "telefone"
|
||||||
|
| "cpfCnpj"
|
||||||
|
| "cep"
|
||||||
|
| "select"
|
||||||
|
| CampoTipoNumerico;
|
||||||
|
|
||||||
6
src/tipos/cartao.ts
Normal file
6
src/tipos/cartao.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
/**
|
||||||
|
* Tipos do componente EliCartao.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type CartaoStatus = "novo" | "rascunho" | "vendido" | "cancelado";
|
||||||
|
|
||||||
4
src/tipos/index.ts
Normal file
4
src/tipos/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from "./botao";
|
||||||
|
export * from "./cartao";
|
||||||
|
export * from "./campo";
|
||||||
|
export * from "./indicador";
|
||||||
29
src/tipos/indicador.ts
Normal file
29
src/tipos/indicador.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Tipos do componente EliBadge (indicador).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type IndicadorLocalizacao =
|
||||||
|
| "top right"
|
||||||
|
| "right center"
|
||||||
|
| "bottom right"
|
||||||
|
| "top center"
|
||||||
|
| "bottom center"
|
||||||
|
| "top left"
|
||||||
|
| "left center"
|
||||||
|
| "bottom left";
|
||||||
|
|
||||||
|
export type IndicadorOffset =
|
||||||
|
| "-20"
|
||||||
|
| "-15"
|
||||||
|
| "-10"
|
||||||
|
| "-5"
|
||||||
|
| "0"
|
||||||
|
| "20"
|
||||||
|
| "15"
|
||||||
|
| "10"
|
||||||
|
| "5";
|
||||||
|
|
||||||
|
export type IndicadorPresetRaio = "suave" | "pill";
|
||||||
|
|
||||||
|
export type CssLength = `${number}px` | `${number}rem` | `${number}%` | "0";
|
||||||
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
|
|
||||||
/* Tipagem */
|
/* Tipagem */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"jsx": "preserve",
|
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|
@ -27,6 +26,6 @@
|
||||||
/* Vue */
|
/* Vue */
|
||||||
"types": ["vite/client"]
|
"types": ["vite/client"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"],
|
||||||
"exclude": ["dist", "node_modules"]
|
"exclude": ["dist", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue