Compare commits
3 commits
master
...
master-eli
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fdc5a95ce | |||
| 6c84508996 | |||
| 454fddb061 |
13 changed files with 980 additions and 13 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;
|
||||||
312
src/componentes/EliInput/EliInput.vue
Normal file
312
src/componentes/EliInput/EliInput.vue
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
<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 options"
|
||||||
|
:key="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 options"
|
||||||
|
:key="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 { formatarCpfCnpj } from "./utils/cpfCnpj";
|
||||||
|
import { formatTelefone } from "./utils/telefone";
|
||||||
|
import { formatarDecimal, formatarMoeda, somenteNumeros } from "./utils/numerico"
|
||||||
|
import { formatarCep } from "./utils/cep";
|
||||||
|
|
||||||
|
type Option = {
|
||||||
|
label: string;
|
||||||
|
value: any;
|
||||||
|
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;
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "EliInput",
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
modelValue: { type: [String, Number, Array] as any, default: "" },
|
||||||
|
type: { type: String as PropType<InputType>, default: "text" },
|
||||||
|
label: String,
|
||||||
|
placeholder: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
error: Boolean,
|
||||||
|
errorMessages: { type: [String, Array] as any, default: () => [] },
|
||||||
|
hint: String,
|
||||||
|
persistentHint: Boolean,
|
||||||
|
rows: { type: Number, default: 4 },
|
||||||
|
options: { type: Array as PropType<Option[]>, default: () => [] },
|
||||||
|
clearable: Boolean,
|
||||||
|
variant: { type: String as PropType<InputVariant>, default: "outlined" },
|
||||||
|
density: { type: String as PropType<Density>, 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) => {
|
||||||
|
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(() => {
|
||||||
|
// 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 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
value,
|
||||||
|
isTextLike,
|
||||||
|
inputHtmlType,
|
||||||
|
inputMode,
|
||||||
|
internalColor,
|
||||||
|
showPassword,
|
||||||
|
togglePassword,
|
||||||
|
onInput,
|
||||||
|
onFocus: () => emit("focus"),
|
||||||
|
onBlur: () => emit("blur"),
|
||||||
|
computedItems,
|
||||||
|
optLabel,
|
||||||
|
optValue,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.eli-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
116
src/componentes/EliInput/README.md
Normal file
116
src/componentes/EliInput/README.md
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
# 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 Option = { label: string; value: any; 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 \| any[]` | `""` | 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.
|
||||||
4
src/componentes/EliInput/index.ts
Normal file
4
src/componentes/EliInput/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import EliInput from "./EliInput.vue";
|
||||||
|
|
||||||
|
export { EliInput };
|
||||||
|
export default EliInput;
|
||||||
9
src/componentes/EliInput/utils/cep.ts
Normal file
9
src/componentes/EliInput/utils/cep.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { somenteNumeros } from "./numerico";
|
||||||
|
|
||||||
|
export function formatarCep(v: string): string {
|
||||||
|
const d = somenteNumeros(v).slice(0, 8);
|
||||||
|
|
||||||
|
if (d.length <= 5) return d;
|
||||||
|
|
||||||
|
return d.replace(/^(\d{5})(\d{1,3})$/, "$1-$2");
|
||||||
|
}
|
||||||
24
src/componentes/EliInput/utils/cpfCnpj.ts
Normal file
24
src/componentes/EliInput/utils/cpfCnpj.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
function somenteNumeros(v: string): string {
|
||||||
|
return v.replace(/\D+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatarCpfCnpj(v: string): string {
|
||||||
|
const d = somenteNumeros(v);
|
||||||
|
|
||||||
|
// CPF
|
||||||
|
if (d.length <= 11) {
|
||||||
|
return d
|
||||||
|
.replace(/(\d{3})(\d)/, "$1.$2")
|
||||||
|
.replace(/(\d{3})(\d)/, "$1.$2")
|
||||||
|
.replace(/(\d{3})(\d{1,2})$/, "$1-$2")
|
||||||
|
.slice(0, 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CNPJ
|
||||||
|
return d
|
||||||
|
.replace(/^(\d{2})(\d)/, "$1.$2")
|
||||||
|
.replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3")
|
||||||
|
.replace(/\.(\d{3})(\d)/, ".$1/$2")
|
||||||
|
.replace(/(\d{4})(\d)/, "$1-$2")
|
||||||
|
.slice(0, 18);
|
||||||
|
}
|
||||||
17
src/componentes/EliInput/utils/numerico.ts
Normal file
17
src/componentes/EliInput/utils/numerico.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
export function somenteNumeros(valor: string) {
|
||||||
|
return valor.replace(/\D+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatarDecimal(valor: string) {
|
||||||
|
const limpo = valor.replace(/[^\d,]/g, "");
|
||||||
|
const partes = limpo.split(",");
|
||||||
|
return partes.length > 2 ? partes[0] + "," + partes.slice(1).join("") : limpo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatarMoeda(valor: string) {
|
||||||
|
const numero = somenteNumeros(valor);
|
||||||
|
if (!numero) return "";
|
||||||
|
|
||||||
|
const inteiro = (parseInt(numero, 10) / 100).toFixed(2);
|
||||||
|
return inteiro.replace(".", ",").replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
||||||
|
}
|
||||||
31
src/componentes/EliInput/utils/telefone.ts
Normal file
31
src/componentes/EliInput/utils/telefone.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
// utils/telefone.ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove tudo que não é número
|
||||||
|
*/
|
||||||
|
export function sanitizeTelefone(value: string): string {
|
||||||
|
return value.replace(/\D+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aplica máscara dinâmica de telefone BR
|
||||||
|
*/
|
||||||
|
export function formatTelefone(value: string): string {
|
||||||
|
const digits = sanitizeTelefone(value);
|
||||||
|
|
||||||
|
if (!digits) return "";
|
||||||
|
|
||||||
|
// (99) 9999-9999
|
||||||
|
if (digits.length <= 10) {
|
||||||
|
return digits
|
||||||
|
.replace(/^(\d{2})(\d)/, "($1) $2")
|
||||||
|
.replace(/(\d{4})(\d)/, "$1-$2")
|
||||||
|
.slice(0, 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (99) 99999-9999
|
||||||
|
return digits
|
||||||
|
.replace(/^(\d{2})(\d)/, "($1) $2")
|
||||||
|
.replace(/(\d{5})(\d)/, "$1-$2")
|
||||||
|
.slice(0, 15);
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,164 @@
|
||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<v-card class="mx-auto" max_width="400">
|
<v-card class="mx-auto" max_width="400">
|
||||||
<v-card-title>Olá Mundo!</v-card-title>
|
<v-card-title>
|
||||||
|
<EliBadge :badge="'Novo'" offset-x="-15" location="right center">
|
||||||
|
Olá Mundo!
|
||||||
|
</EliBadge>
|
||||||
|
</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
Este é um componente de exemplo integrado com Vuetify.
|
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-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-btn color="primary" variant="elevated" block style="padding: 10px;">
|
<EliBotao color="primary" variant="elevated" block>
|
||||||
Botão Vuetify
|
Botão Vuetify
|
||||||
</v-btn>
|
</EliBotao>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue"
|
import { defineComponent, ref } from "vue";
|
||||||
|
import EliBotao from "../EliBotao/EliBotao.vue";
|
||||||
|
import EliBadge from "../EliBadge/EliBadge.vue";
|
||||||
|
import EliInput from "../EliInput/EliInput.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "EliOlaMundo",
|
name: "EliOlaMundo",
|
||||||
})
|
components: {
|
||||||
|
EliBotao,
|
||||||
|
EliBadge,
|
||||||
|
EliInput,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const nome = ref("");
|
||||||
|
const estado = ref([])
|
||||||
|
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(null);
|
||||||
|
const habilidades = ref<any[]>([]);
|
||||||
|
return {
|
||||||
|
nome,
|
||||||
|
email,
|
||||||
|
documento,
|
||||||
|
estado,
|
||||||
|
telefone,
|
||||||
|
mensagem,
|
||||||
|
senha,
|
||||||
|
cor,
|
||||||
|
habilidades,
|
||||||
|
idade,
|
||||||
|
altura,
|
||||||
|
cep,
|
||||||
|
valor,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
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";
|
||||||
|
import { EliInput } from "./componentes/EliInput";
|
||||||
|
|
||||||
export { EliOlaMundo };
|
export { EliOlaMundo };
|
||||||
export { EliBotao };
|
export { EliBotao };
|
||||||
|
export { EliBadge };
|
||||||
|
export { EliInput };
|
||||||
|
|
||||||
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);
|
||||||
|
app.component("EliInput", EliInput);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<EliOlaMundo />
|
<EliOlaMundo />
|
||||||
<EliBotao
|
|
||||||
color="primary"
|
|
||||||
@click="() => {console.log('xxx')}"
|
|
||||||
>
|
|
||||||
Button
|
|
||||||
</EliBotao>
|
|
||||||
</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';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
EliOlaMundo,
|
EliOlaMundo,
|
||||||
EliBotao,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue