Compare commits

..

No commits in common. "master-eli-badge" and "master" have entirely different histories.

39 changed files with 199 additions and 1668 deletions

197
.agent
View file

@ -1,197 +0,0 @@
# 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

1
.gitignore vendored
View file

@ -1,2 +1 @@
node_modules node_modules
dist

View file

@ -1,63 +1,5 @@
# eli-vue — Design System (Vue 3 + TypeScript) # Vue 3 + TypeScript + Vite
Biblioteca de componentes Vue 3 (Design System) para reutilização em múltiplos projetos, com foco em: 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.
- consistência visual e comportamental 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).
- tipagem forte (TypeScript `strict`)
- documentação em português
- exemplos executáveis via playground
As regras do repositório estão descritas em **`.agent`**.
## Instalação
Como dependência do projeto:
```bash
pnpm add eli-vue
```
> Observação: `vue` e `vuetify` são **peerDependencies**. Garanta que seu projeto já os tenha instalados.
## 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");
```
### 2) Importação direta de componentes
```ts
import { EliBotao, EliInput, EliBadge } from "eli-vue";
```
## Convenções do projeto
- Componentes usam **prefixo `Eli`** (ex.: `EliBotao`, `EliInput`).
- Pastas seguem **português** (ex.: `src/componentes/botao`, `src/componentes/campo`, `src/componentes/indicador`).
- Sem TSX; padrão SFC: `<template>` + `<script lang="ts">` + `defineComponent`.
- Evitar `any`.
## Como criar um novo componente (checklist)
1. Criar pasta em `src/componentes/<nome_em_portugues>/`
2. Criar `EliNomeDoComponente.vue` com `defineComponent` + comentários úteis
3. Criar `index.ts` re-exportando o componente
4. Criar `README.md` do componente (API, exemplos, casos de borda)
5. Criar playground em `src/playground/<nome>.playground.vue` (3+ variações)
6. Exportar no `src/index.ts`
## Como rodar o playground
```bash
pnpm dev
```
O playground fica em `src/playground` e serve para validar visualmente os componentes durante o desenvolvimento.

62
dist/eli-vue.es.js vendored Normal file
View file

@ -0,0 +1,62 @@
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 Normal file
View file

@ -0,0 +1 @@
(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"}})}));

View file

@ -0,0 +1,3 @@
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;

View file

@ -0,0 +1,3 @@
import EliOlaMundo from './EliOlaMundo.vue';
export { EliOlaMundo };
export default EliOlaMundo;

7
dist/types/index.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
import type { App } from "vue";
import { EliOlaMundo } from './componentes/EliOlaMundo';
export { EliOlaMundo };
declare const _default: {
install(app: App): void;
};
export default _default;

View file

@ -14,7 +14,19 @@
<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",
@ -28,12 +40,12 @@ export default defineComponent({
}, },
variant: { variant: {
type: String as PropType<BotaoVariante>, type: String as PropType<BotaoVariant>,
default: "elevated", default: "elevated",
}, },
size: { size: {
type: String as PropType<BotaoTamanho>, type: String as PropType<BotaoSize>,
default: "default", default: "default",
}, },

View file

@ -116,7 +116,7 @@ Exemplos:
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue' import { defineComponent, ref } from 'vue'
import EliBotao from '@/componentes/botao/EliBotao.vue' import EliBotao from '@/components/EliBotao.vue'
export default defineComponent({ export default defineComponent({
components: { EliBotao }, components: { EliBotao },

View file

@ -0,0 +1,4 @@
import EliBotao from "./EliBotao.vue";
export { EliBotao };
export default EliBotao;

View file

@ -0,0 +1,23 @@
<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>

View file

@ -0,0 +1,54 @@
# 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)
}
}
```

View file

@ -0,0 +1,4 @@
import EliOlaMundo from './EliOlaMundo.vue'
export { EliOlaMundo }
export default EliOlaMundo

View file

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

View file

@ -1,297 +0,0 @@
<template>
<div class="eli-input">
<!-- TEXT LIKE INPUTS -->
<v-text-field
v-if="isTextLike"
v-model="value"
:type="inputHtmlType"
:label="label"
:placeholder="placeholder"
:disabled="disabled"
:clearable="clearable && type !== 'password'"
:error="error"
:error-messages="errorMessages"
:hint="hint"
:persistent-hint="persistentHint"
:density="density"
:variant="variant"
:color="internalColor"
:inputmode="inputMode"
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 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>

View file

@ -1,117 +0,0 @@
# EliInput
**Componente base de input do design system**
O EliInput unifica vários tipos de campo (v-text-field, v-textarea, v-select, v-radio-group, v-checkbox) em uma única API consistente. Ele encapsula comportamentos, máscaras e regras comuns (CPF/CNPJ, telefone, CEP, numéricos, formatação de moeda etc.) para manter coerência visual e de lógica em toda a aplicação.
> ⚠️ Nunca use os componentes Vuetify diretamente fora do design system para esses casos.
> Utilize sempre EliInput para garantir formatação, tipagem e repasse de atributos padronizados.
---
## Visão geral
EliInput foi projetado para:
* Centralizar formatação (máscaras) para tipos de entrada comuns (telefone, CPF/CNPJ, CEP, numéricos).
* Fornecer uma API única (type) que representa diferentes controles (text, textarea, select, radio, checkbox, etc.).
* Repassar atributos/props do pai para o componente Vuetify interno (v-bind="$attrs") mantendo inheritAttrs: false.
* Emitir eventos padronizados: update:modelValue, change, focus, blur.
---
## Principais decisões de implementação
* inheritAttrs: false — o componente controla explicitamente para onde os atributos são passados (v-bind="attrs" no elemento interno).
* Uso de um computed value que faz emit("update:modelValue", v) e emit("change", v) — assim qualquer v-model no pai funciona como esperado.
* Normalização de props.options para aceitar objetos { label, value, disabled } ou primitivos ('A', 1).
* Separação clara entre lógica de formatação (aplicada em onInput) e componentes que não devem ser formatados (ex.: v-select).
---
## Tipagem (TypeScript)
```ts
type ValorCampo = string | number | boolean | null;
type Option = { label: string; value: ValorCampo; disabled?: boolean };
type InputVariant = 'outlined' | 'filled' | 'plain' | 'solo' | 'solo-filled' | 'solo-inverted' | 'underlined';
type Density = 'default' | 'comfortable' | 'compact';
type TipoNumerico = 'numericoInteiro' | 'numericoDecimal' | 'numericoMoeda';
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.

View file

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

View file

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

View file

@ -1,24 +0,0 @@
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);
}

View file

@ -1,17 +0,0 @@
export function somenteNumeros(valor: string) {
return valor.replace(/\D+/g, "");
}
export function formatarDecimal(valor: string) {
const limpo = valor.replace(/[^\d,]/g, "");
const partes = limpo.split(",");
return partes.length > 2 ? partes[0] + "," + partes.slice(1).join("") : limpo;
}
export function formatarMoeda(valor: string) {
const numero = somenteNumeros(valor);
if (!numero) return "";
const inteiro = (parseInt(numero, 10) / 100).toFixed(2);
return inteiro.replace(".", ",").replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}

View file

@ -1,31 +0,0 @@
// 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);
}

View file

@ -1,118 +0,0 @@
<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>

View file

@ -1,182 +0,0 @@
# 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.

View file

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

View file

@ -1,166 +0,0 @@
<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>

View file

@ -1,34 +0,0 @@
# 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

View file

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

View file

@ -1,21 +1,13 @@
import type { App, Plugin } from "vue"; import type { App } from "vue";
import { EliOlaMundo } from "./componentes/ola_mundo"; import { EliOlaMundo } from "./componentes/EliOlaMundo";
import { EliBotao } from "./componentes/botao"; import { EliBotao } from "./componentes/EliBotao";
import { EliBadge } from "./componentes/indicador";
import { EliInput } from "./componentes/campo";
export { EliOlaMundo }; export { EliOlaMundo };
export { EliBotao }; export { EliBotao };
export { EliBadge };
export { EliInput };
const EliVue: Plugin = { export default {
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);
}, },
}; };
export default EliVue;

View file

@ -1,42 +1,23 @@
<template> <template>
<v-container class="py-6"> <EliOlaMundo />
<h1 class="text-h5 mb-4">Playground eli-vue</h1> <EliBotao
color="primary"
<v-tabs v-model="aba" color="primary" density="comfortable"> @click="() => {console.log('xxx')}"
<v-tab value="botao">Botão</v-tab> >
<v-tab value="indicador">Indicador</v-tab> Button
<v-tab value="campo">Campo</v-tab> </EliBotao>
<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'" />
<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 BotaoPlayground from "./botao.playground.vue"; import { EliOlaMundo } from '@/componentes/EliOlaMundo'
import IndicadorPlayground from "./indicador.playground.vue"; import {EliBotao} from '@/componentes/EliBotao';
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: {
BotaoPlayground, EliOlaMundo,
IndicadorPlayground, EliBotao,
CampoPlayground,
OlaMundoPlayground,
},
data() {
return {
aba: "botao" as "botao" | "indicador" | "campo" | "ola_mundo",
};
} }
}) })
</script> </script>

View file

@ -1,45 +0,0 @@
<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>

View file

@ -1,123 +0,0 @@
<template>
<section class="stack">
<h2>EliInput (campo)</h2>
<div class="grid">
<EliInput v-model="nome" label="Nome" placeholder="Digite seu nome" />
<EliInput v-model="telefone" type="telefone" label="Telefone" />
<EliInput v-model="documento" type="cpfCnpj" label="CPF / CNPJ" />
<EliInput v-model="idade" type="numericoInteiro" label="Idade" />
<EliInput v-model="valor" type="numericoMoeda" label="Valor" />
<EliInput v-model="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>

View file

@ -1,65 +0,0 @@
<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>

View file

@ -1,34 +0,0 @@
<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>

View file

@ -1,9 +0,0 @@
/**
* 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";

View file

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

View file

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

View file

@ -1,29 +0,0 @@
/**
* 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";

View file

@ -9,6 +9,7 @@
/* Tipagem */ /* Tipagem */
"strict": true, "strict": true,
"jsx": "preserve",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
@ -26,6 +27,6 @@
/* Vue */ /* Vue */
"types": ["vite/client"] "types": ["vite/client"]
}, },
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"], "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["dist", "node_modules"] "exclude": ["dist", "node_modules"]
} }