chore: alinhar projeto às regras do agent

This commit is contained in:
Luiz Silva 2026-01-02 21:16:50 -03:00
parent 86d451efa1
commit 51a48eee70
36 changed files with 485 additions and 208 deletions

View file

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

View file

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

View file

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

View file

@ -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)
}
}
```

View file

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

View file

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

View file

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

View file

@ -76,8 +76,8 @@
:row="row"
>
<v-radio
v-for="opt in options"
:key="opt.value"
v-for="opt in computedItems"
:key="String(opt.value)"
:label="opt.label"
:value="opt.value"
/>
@ -86,8 +86,8 @@
<!-- CHECKBOX -->
<div v-else-if="type === 'checkbox'" class="checkbox-group">
<v-checkbox
v-for="opt in options"
:key="opt.value"
v-for="opt in computedItems"
:key="String(opt.value)"
v-model="value"
:label="opt.label"
:value="opt.value"
@ -104,12 +104,27 @@ import { formatTelefone } from "./utils/telefone";
import { formatarDecimal, formatarMoeda, somenteNumeros } from "./utils/numerico"
import { formatarCep } from "./utils/cep";
type Option = {
/**
* Tipos base do EliInput.
* Mantidos aqui para evitar exports internos não documentados.
*/
type ValorCampo = string | number | boolean | null;
type ValorCampoMultiplo = ValorCampo[];
type OpcaoCampo<TValor extends ValorCampo = ValorCampo> = {
label: string;
value: any;
value: TValor;
disabled?: boolean;
};
type OpcaoBruta<TValor extends ValorCampo = ValorCampo> =
| TValor
| {
label?: string;
value: TValor;
disabled?: boolean;
};
type InputVariant =
| "outlined"
| "filled"
@ -146,17 +161,34 @@ export default defineComponent({
inheritAttrs: false,
props: {
modelValue: { type: [String, Number, Array] as any, default: "" },
/**
* 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<ValorCampo | ValorCampoMultiplo>,
default: "",
},
type: { type: String as PropType<InputType>, default: "text" },
label: String,
placeholder: String,
disabled: Boolean,
error: Boolean,
errorMessages: { type: [String, Array] as any, default: () => [] },
errorMessages: {
type: [String, Array] as PropType<string | string[]>,
default: () => [],
},
hint: String,
persistentHint: Boolean,
rows: { type: Number, default: 4 },
options: { type: Array as PropType<Option[]>, default: () => [] },
/**
* Para select/radio/checkbox.
* Aceita lista normalizada ({ label, value }) ou valores primitivos.
*/
options: {
type: Array as PropType<Array<OpcaoBruta>>,
default: () => [],
},
clearable: Boolean,
variant: { type: String as PropType<InputVariant>, default: "outlined" },
density: { type: String as PropType<Density>, default: "comfortable" },
@ -175,7 +207,7 @@ export default defineComponent({
const value = computed({
get: () => props.modelValue,
set: (v) => {
set: (v: ValorCampo | ValorCampoMultiplo) => {
emit("update:modelValue", v);
emit("change", v);
},
@ -255,25 +287,23 @@ export default defineComponent({
}
// --- Helpers para select / radio / checkbox (aceita objetos ou primitivos) ---
const computedItems = computed(() => {
const computedItems = computed<Array<OpcaoCampo>>(() => {
// Normaliza options para [{ label, value, disabled? }]
return (props.options || []).map((o: any) =>
o && typeof o === "object" && ("label" in o || "value" in o)
? { label: o.label ?? String(o.value), value: o.value, disabled: o.disabled }
: { label: String(o), value: o }
);
return (props.options || []).map((o) => {
if (o && typeof o === "object" && "value" in o) {
const valor = o.value as ValorCampo;
return {
label: o.label ?? String(valor),
value: valor,
disabled: o.disabled,
};
}
const valor = o as ValorCampo;
return { label: String(valor), value: valor };
});
});
function optLabel(opt: any) {
if (opt && typeof opt === "object") return opt.label ?? String(opt.value);
return String(opt);
}
function optValue(opt: any) {
if (opt && typeof opt === "object") return opt.value;
return opt;
}
return {
attrs,
@ -288,8 +318,6 @@ export default defineComponent({
onFocus: () => emit("focus"),
onBlur: () => emit("blur"),
computedItems,
optLabel,
optValue,
};
},
});

View file

@ -32,7 +32,8 @@ EliInput foi projetado para:
## Tipagem (TypeScript)
```ts
type Option = { label: string; value: any; disabled?: boolean };
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';
@ -50,7 +51,7 @@ type InputType =
| Prop | Tipo | Default | Descrição |
| ---------------- | --------------------------- | --------------- | ------------------------------------------------------ |
| `modelValue` | `string \| number \| any[]` | `""` | Valor controlado (use com `v-model`). |
| `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. |
@ -113,4 +114,4 @@ type InputType =
## 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.
* 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

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

View file

@ -121,8 +121,8 @@ export default defineComponent({
</script>
<style scoped>
::v-deep .v-badge__badge,
::v-deep .v-badge__content {
:deep(.v-badge__badge),
:deep(.v-badge__content) {
border-radius: var(--eli-badge-radius) !important;
}
</style>

View file

@ -177,6 +177,6 @@ Exemplo (pseudocódigo):
Observações sobre Vuetify
1. O EliBadge usa seletores com ::v-deep para alterar o border-radius do elemento interno do v-badge. Isso funciona para Vuetify 2 e 3, mas as classes internas podem variar entre versões. Se você atualizar o Vuetify, verifique os nomes de classe (.v-badge__badge ou .v-badge__content) e ajuste o seletor se necessário.
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

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

View file

@ -119,9 +119,11 @@
<script lang="ts">
import { defineComponent, ref } from "vue";
import EliBotao from "../EliBotao/EliBotao.vue";
import EliBadge from "../EliBadge/EliBadge.vue";
import EliInput from "../EliInput/EliInput.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",
@ -132,7 +134,7 @@ export default defineComponent({
},
setup() {
const nome = ref("");
const estado = ref([])
const estado = ref<string[]>([]);
const cep = ref("");
const telefone = ref("");
const idade = ref("");
@ -142,8 +144,8 @@ export default defineComponent({
const mensagem = ref("");
const senha = ref("");
const documento = ref("");
const cor = ref(null);
const habilidades = ref<any[]>([]);
const cor = ref<"azul" | "verde" | null>(null);
const habilidades = ref<Habilidade[]>([]);
return {
nome,
email,

View 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

View file

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

View file

@ -1,15 +1,15 @@
import type { App } from "vue";
import { EliOlaMundo } from "./componentes/EliOlaMundo";
import { EliBotao } from "./componentes/EliBotao";
import { EliBadge } from "./componentes/EliBadge";
import { EliInput } from "./componentes/EliInput";
import type { App, Plugin } from "vue";
import { EliOlaMundo } from "./componentes/ola_mundo";
import { EliBotao } from "./componentes/botao";
import { EliBadge } from "./componentes/indicador";
import { EliInput } from "./componentes/campo";
export { EliOlaMundo };
export { EliBotao };
export { EliBadge };
export { EliInput };
export default {
const EliVue: Plugin = {
install(app: App) {
app.component("EliOlaMundo", EliOlaMundo);
app.component("EliBotao", EliBotao);
@ -17,3 +17,5 @@ export default {
app.component("EliInput", EliInput);
},
};
export default EliVue;

View file

@ -1,16 +1,42 @@
<template>
<EliOlaMundo />
<v-container class="py-6">
<h1 class="text-h5 mb-4">Playground eli-vue</h1>
<v-tabs v-model="aba" color="primary" density="comfortable">
<v-tab value="botao">Botão</v-tab>
<v-tab value="indicador">Indicador</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'" />
<CampoPlayground v-else-if="aba === 'campo'" />
<OlaMundoPlayground v-else />
</v-container>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { EliOlaMundo } from '@/componentes/EliOlaMundo'
import BotaoPlayground from "./botao.playground.vue";
import IndicadorPlayground from "./indicador.playground.vue";
import CampoPlayground from "./campo.playground.vue";
import OlaMundoPlayground from "./ola_mundo.playground.vue";
export default defineComponent({
name: 'App',
components: {
EliOlaMundo,
BotaoPlayground,
IndicadorPlayground,
CampoPlayground,
OlaMundoPlayground,
},
data() {
return {
aba: "botao" as "botao" | "indicador" | "campo" | "ola_mundo",
};
}
})
</script>

View 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>

View 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>

View 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>

View 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>