feat: componente de badge
This commit is contained in:
parent
751857b170
commit
454fddb061
5 changed files with 328 additions and 1 deletions
128
src/componentes/EliBadge/EliBadge.vue
Normal file
128
src/componentes/EliBadge/EliBadge.vue
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
<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";
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
const RADIUS_MAP: Record<BadgeRadiusPreset, string> = {
|
||||||
|
suave: "4px",
|
||||||
|
pill: "10px",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "EliBadge",
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: "primary",
|
||||||
|
},
|
||||||
|
|
||||||
|
location: {
|
||||||
|
type: String as PropType<LocalBadge>,
|
||||||
|
default: "top right",
|
||||||
|
},
|
||||||
|
|
||||||
|
offsetX: {
|
||||||
|
type: String as PropType<Offset>,
|
||||||
|
default: "0",
|
||||||
|
},
|
||||||
|
|
||||||
|
offsetY: {
|
||||||
|
type: String as PropType<Offset>,
|
||||||
|
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<BadgeRadiusPreset | CssLength>,
|
||||||
|
default: "suave",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
|
||||||
|
const resolvedRadius = computed(() => {
|
||||||
|
// preset conhecido
|
||||||
|
if (props.radius in RADIUS_MAP) {
|
||||||
|
return RADIUS_MAP[props.radius as BadgeRadiusPreset];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
::v-deep .v-badge__badge,
|
||||||
|
::v-deep .v-badge__content {
|
||||||
|
border-radius: var(--eli-badge-radius) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
182
src/componentes/EliBadge/README.md
Normal file
182
src/componentes/EliBadge/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 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.
|
||||||
|
|
||||||
|
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.
|
||||||
4
src/componentes/EliBadge/index.ts
Normal file
4
src/componentes/EliBadge/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import EliBadge from "./EliBadge.vue";
|
||||||
|
|
||||||
|
export { EliBadge };
|
||||||
|
export default EliBadge;
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
import type { App } from "vue";
|
import type { App } from "vue";
|
||||||
import { EliOlaMundo } from "./componentes/EliOlaMundo";
|
import { EliOlaMundo } from "./componentes/EliOlaMundo";
|
||||||
import { EliBotao } from "./componentes/EliBotao";
|
import { EliBotao } from "./componentes/EliBotao";
|
||||||
|
import { EliBadge } from "./componentes/EliBadge";
|
||||||
|
|
||||||
export { EliOlaMundo };
|
export { EliOlaMundo };
|
||||||
export { EliBotao };
|
export { EliBotao };
|
||||||
|
export { EliBadge };
|
||||||
|
|
||||||
export default {
|
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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,28 @@
|
||||||
>
|
>
|
||||||
Button
|
Button
|
||||||
</EliBotao>
|
</EliBotao>
|
||||||
|
<EliBadge
|
||||||
|
badge="Novo"
|
||||||
|
offset-x="-20"
|
||||||
|
location="right center"
|
||||||
|
radius="pill"
|
||||||
|
>
|
||||||
|
Vistoria
|
||||||
|
</EliBadge>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import { EliOlaMundo } from '@/componentes/EliOlaMundo'
|
import { EliOlaMundo } from '@/componentes/EliOlaMundo'
|
||||||
import { EliBotao } from '@/componentes/EliBotao';
|
import { EliBotao } from '@/componentes/EliBotao';
|
||||||
|
import EliBadge from '@/componentes/EliBadge/EliBadge.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
EliOlaMundo,
|
EliOlaMundo,
|
||||||
EliBotao,
|
EliBotao,
|
||||||
|
EliBadge,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue