feat: componente padrao de botao

This commit is contained in:
andreLMpena 2025-12-18 09:46:32 -03:00
parent 35f9d57ce7
commit 751857b170
7 changed files with 416 additions and 17 deletions

View file

@ -0,0 +1,64 @@
<template>
<v-btn
:color="color"
:variant="variant"
:size="size"
:disabled="disabled"
:loading="loading"
v-bind="$attrs"
class="text-none pt-1"
>
<slot />
</v-btn>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
type BotaoVariant =
| "elevated"
| "flat"
| "outlined"
| "text"
| "tonal"
type BotaoSize =
| "x-small"
| "small"
| "default"
| "large"
export default defineComponent({
name: "EliBotao",
inheritAttrs: false,
props: {
color: {
type: String,
default: "primary"
},
variant: {
type: String as PropType<BotaoVariant>,
default: "elevated",
},
size: {
type: String as PropType<BotaoSize>,
default: "default",
},
disabled: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
}
})
</script>

View file

@ -0,0 +1,173 @@
# EliBotao
**Componente base de botão do design system**
O `EliBotao` encapsula o `v-btn` do Vuetify para aplicar padrões visuais, acessibilidade e comportamentos esperados em toda a aplicação.
> ⚠️ **Nunca use `v-btn` diretamente fora do design system.**
> Utilize sempre o `EliBotao`.
---
## Visão geral
O `EliBotao` oferece uma API simples e tipada para botões, controlando atributos, listeners e estilos por padrão. Foi projetado para:
* Garantir consistência visual e de comportamento;
* Facilitar a adoção de boas práticas de A11y;
* Permitir repasse transparente de atributos e listeners ao `v-btn` interno.
### Principais decisões de implementação
* `inheritAttrs: false` — o componente faz `v-bind="$attrs"` manualmente no `v-btn` filho.
* Slot padrão para conteúdo (texto, ícones ou markup).
* Classes base aplicadas: `text-none pt-1` (podem ser sobrescritas via `class`).
---
## Tipagem (TypeScript)
```ts
type BotaoVariant = 'elevated' | 'flat' | 'outlined' | 'text' | 'tonal'
type BotaoSize = 'x-small' | 'small' | 'default' | 'large'
interface EliBotaoProps {
color?: string
variant?: BotaoVariant
size?: BotaoSize
disabled?: boolean
loading?: boolean
}
```
---
## Props
| Prop | Tipo | Default | Descrição |
| ---------- | -------------- | ------------ | ----------------------------------------------------------------- |
| `color` | `string` | `"primary"` | Cor visual do botão (usa as cores registradas no Vuetify). |
| `variant` | `BotaoVariant` | `"elevated"` | Variante visual: `elevated`, `flat`, `outlined`, `text`, `tonal`. |
| `size` | `BotaoSize` | `"default"` | Tamanho do botão (`x-small`, `small`, `default`, `large`). |
| `disabled` | `boolean` | `false` | Desabilita interação e aplica estilo inativo. |
| `loading` | `boolean` | `false` | Exibe indicador de carregamento e bloqueia interação. |
### Notas sobre props
* **`color`**: utilize as cores definidas na configuração do Vuetify para manter consistência.
* **`disabled`** e **`loading`**: quando `true`, o botão deixa de responder a eventos e apresenta feedback visual apropriado.
---
## Repasso de atributos e listeners
O componente define `inheritAttrs: false` e aplica `v-bind="$attrs"` ao `v-btn` interno. Isso implica que:
1. **Atributos HTML** (ex.: `type`, `aria-label`, `class`, `style`) passados para `<EliBotao>` serão aplicados ao `v-btn` filho.
2. **Listeners** (ex.: `@click`, `@mouseover`) também fazem parte de `$attrs` no Vue 3 e são repassados ao `v-btn` — use o componente como se estivesse escutando eventos diretamente no botão.
**Exemplo:**
```vue
<EliBotao @click="onClick" type="button">Salvar</EliBotao>
```
O `onClick` será invocado quando o `v-btn` emitir o evento.
---
## Slot
O `EliBotao` expõe um slot padrão para o conteúdo do botão. Pode ser texto, ícone ou markup mais complexo.
Exemplos:
```vue
<EliBotao>Salvar</EliBotao>
<EliBotao>
<v-icon left>mdi-check</v-icon>
Salvar
</EliBotao>
<EliBotao>
<span>Texto com <strong>destaque</strong></span>
</EliBotao>
```
---
## Exemplo de uso completo
```vue
<template>
<EliBotao
color="primary"
variant="elevated"
:loading="isSaving"
aria-label="Salvar formulário"
@click="salvar"
type="button"
>
<v-icon left>mdi-content-save</v-icon>
Salvar
</EliBotao>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import EliBotao from '@/components/EliBotao.vue'
export default defineComponent({
components: { EliBotao },
setup() {
const isSaving = ref(false)
function salvar() {
// lógica de salvamento
}
return { salvar, isSaving }
}
})
</script>
```
---
## Acessibilidade (A11y)
* Sempre forneça `aria-label` quando o botão exibir apenas um ícone.
* Para botões de ação em formulários, defina explicitamente `type="button"` ou `type="submit"` conforme apropriado.
* Evite usar apenas `color` para transmitir significado; combine com texto e atributos ARIA quando necessário.
---
## Boas práticas
* **Consistência visual**: não aplique estilos inline no lugar de tokens do design system — prefira classes e variáveis do Vuetify.
* **Overrides de classe**: a classe padrão `text-none pt-1` pode ser sobrescrita via `class` em `$attrs` (ex.: `<EliBotao class="ma-2">`).
* **Estados**: gerencie `loading` e `disabled` no nível do componente pai quando necessário para evitar condições de corrida.
---
## Testes
O `EliBotao` é intencionalmente simples e fácil de testar:
* Monte o componente em testes unitários e verifique que `$attrs` é repassado ao `v-btn`.
* Asserte que `loading` exibe o spinner e que `disabled` impede emissão de eventos.
Exemplo (pseudocódigo):
```ts
const wrapper = mount(EliBotao, { attrs: { 'aria-label': 'Salvar', type: 'button' } })
expect(wrapper.findComponent({ name: 'v-btn' }).attributes('aria-label')).toBe('Salvar')
```
---
## Observações finais
* O componente foi projetado para ser um adaptador seguro para `v-btn`: aplica padrões do design system e centraliza comportamento comum.
* Se precisar estender o componente (por exemplo, adicionar variantes visuais específicas do produto), prefira criar wrappers ou classes utilitárias no design system ao invés de alterar o `EliBotao` diretamente.
---

View file

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

View file

@ -6,7 +6,7 @@
Este é um componente de exemplo integrado com Vuetify.
</v-card-text>
<v-card-actions>
<v-btn color="primary" block>
<v-btn color="primary" variant="elevated" block style="padding: 10px;">
Botão Vuetify
</v-btn>
</v-card-actions>

View file

@ -1,11 +1,13 @@
import type { App } from "vue";
import { EliOlaMundo } from "./componentes/EliOlaMundo";
import { EliBotao } from "./componentes/EliBotao";
import type { App } from "vue"
import { EliOlaMundo } from './componentes/EliOlaMundo'
export { EliOlaMundo }
export { EliOlaMundo };
export { EliBotao };
export default {
install(app: App) {
app.component('EliOlaMundo', EliOlaMundo)
app.component("EliOlaMundo", EliOlaMundo);
app.component("EliBotao", EliBotao);
},
}
};

View file

@ -1,15 +1,23 @@
<template>
<EliOlaMundo />
<EliBotao
color="primary"
@click="() => {console.log('xxx')}"
>
Button
</EliBotao>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { EliOlaMundo } from '@/componentes/EliOlaMundo'
import {EliBotao} from '@/componentes/EliBotao';
export default defineComponent({
name: 'App',
components: {
EliOlaMundo
EliOlaMundo,
EliBotao,
}
})
</script>

View file

@ -1,16 +1,164 @@
import { createApp } from 'vue'
import App from './App.vue'
import { createApp } from "vue";
import App from "./App.vue";
// Vuetify
import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import "vuetify/styles";
import "@mdi/font/css/materialdesignicons.css";
import { createVuetify } from "vuetify";
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";
const themeDefault = {
dark: false,
colors: {
//Primary Color
primary: "#146C43",
"primary-dark": "#0F5132",
"primary-light": "#D2F4EA",
//Secondary Color
secondary: "#6C757D",
"secondary-dark": "#54595E",
"secondary-light": "#ABB5BE",
//Status Color
success: "#28A745",
error: "#DC3545",
warning: "#FFC107",
info: "#17A2B8",
//Gray Color
"gray-100": "#F8F9FA",
"gray-200": "#E9ECEF",
"gray-300": "#DEE2E6",
"gray-400": "#CED4DA",
"gray-500": "#ADB5BD",
"gray-600": "#6C757D",
"gray-700": "#495057",
"gray-800": "#343A40",
"gray-900": "#212529",
//blue Color
"blue-100": "#CFE2FF",
"blue-200": "#9EC5FE",
"blue-300": "#6EA8FE",
"blue-400": "#3D8BFD",
"blue-500": "#0D6EFD",
"blue-600": "#0A58CA",
"blue-700": "#084298",
"blue-800": "#052C65",
"blue-900": "#031633",
//Indigo Color
"indigo-100": "#E0CFFC",
"indigo-200": "#C29FFA",
"indigo-300": "#A370F7",
"indigo-400": "#8540F5",
"indigo-500": "#6610F2",
"indigo-600": "#520DC2",
"indigo-700": "#3D0A91",
"indigo-800": "#290661",
"indigo-900": "#140330",
//Purple Color
"purple-100": "#E2D9F3",
"purple-200": "#C5B3E6",
"purple-300": "#A98EDA",
"purple-400": "#8C68CD",
"purple-500": "#6F42C1",
"purple-600": "#59359A",
"purple-700": "#432874",
"purple-800": "#2C1A4D",
"purple-900": "#160D27",
//Pink Color
"pink-100": "#F7D6E6",
"pink-200": "#EFADCE",
"pink-300": "#E685B5",
"pink-400": "#DE5C9D",
"pink-500": "#D63384",
"pink-600": "#AB296A",
"pink-700": "#801F4F",
"pink-800": "#561435",
"pink-900": "#2B0A1A",
//Red Color
"red-100": "#F8D7DA",
"red-200": "#F1AEB5",
"red-300": "#EA868F",
"red-400": "#E35D6A",
"red-500": "#DC3545",
"red-600": "#B02A37",
"red-700": "#842029",
"red-800": "#58151C",
"red-900": "#2C0B0E",
//Orange Color
"orange-100": "#FFE5D0",
"orange-200": "#FECBA1",
"orange-300": "#FEB272",
"orange-400": "#FD9843",
"orange-500": "#FD7E14",
"orange-600": "#CA6510",
"orange-700": "#984C0C",
"orange-800": "#653208",
"orange-900": "#331904",
//Yellow Color
"yellow-100": "#FFF3CD",
"yellow-200": "#FFE69C",
"yellow-300": "#FFDA6A",
"yellow-400": "#FFCD39",
"yellow-500": "#FFC107",
"yellow-600": "#CC9A06",
"yellow-700": "#997404",
"yellow-800": "#664D03",
"yellow-900": "#664D03",
//Green Color
"green-100": "#D1E7DD",
"green-200": "#A3CFBB",
"green-300": "#75B798",
"green-400": "#479F76",
"green-500": "#198754",
"green-600": "#146C43",
"green-700": "#0F5132",
"green-800": "#0A3622",
"green-900": "#051B11",
//Teal Color
"teal-100": "#D2F4EA",
"teal-200": "#A6E9D5",
"teal-300": "#79DFC1",
"teal-400": "#4DD4AC",
"teal-500": "#20C997",
"teal-600": "#1AA179",
"teal-700": "#13795B",
"teal-800": "#0D503C",
"teal-900": "#06281E",
//Cyan Color
"cyan-100": "#CFF4FC",
"cyan-200": "#9EEAF9",
"cyan-300": "#6EDFF6",
"cyan-400": "#3DD5F3",
"cyan-500": "#0DCAF0",
"cyan-600": "#0AA2C0",
"cyan-700": "#087990",
"cyan-800": "#055160",
"cyan-900": "#032830",
},
};
const vuetify = createVuetify({
components,
directives,
})
theme: {
defaultTheme: "themeDefault",
themes: {
themeDefault,
},
},
});
createApp(App).use(vuetify).mount('#app')
createApp(App).use(vuetify).mount("#app");