feat(cartao): adicionar cards com status e playground tipo trello
This commit is contained in:
parent
ab15c51a1b
commit
5bb6732b81
8 changed files with 438 additions and 2 deletions
148
src/componentes/cartao/EliCartao.vue
Normal file
148
src/componentes/cartao/EliCartao.vue
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
<template>
|
||||||
|
<v-card
|
||||||
|
class="eli-cartao"
|
||||||
|
:variant="variant"
|
||||||
|
:class="classeStatus"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<v-card-title class="eli-cartao__titulo">
|
||||||
|
<div class="eli-cartao__titulo-texto">
|
||||||
|
<slot name="titulo">
|
||||||
|
{{ titulo }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Indicador de status (badge) padronizado pelo design system -->
|
||||||
|
<div class="eli-cartao__status">
|
||||||
|
<EliBadge
|
||||||
|
:badge="rotuloStatus"
|
||||||
|
radius="pill"
|
||||||
|
:color="corStatus"
|
||||||
|
>
|
||||||
|
<span />
|
||||||
|
</EliBadge>
|
||||||
|
</div>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="eli-cartao__conteudo">
|
||||||
|
<slot />
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions v-if="$slots.acoes" class="eli-cartao__acoes">
|
||||||
|
<slot name="acoes" />
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType } from "vue";
|
||||||
|
import type { CartaoStatus } from "../../tipos";
|
||||||
|
import { EliBadge } from "../indicador";
|
||||||
|
|
||||||
|
type CartaoVariante = "outlined" | "flat" | "elevated" | "tonal";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EliCartao
|
||||||
|
*
|
||||||
|
* Um cartão de domínio para listas/pipelines (ex.: oportunidades/propostas) com:
|
||||||
|
* - título
|
||||||
|
* - status padronizado (novo/rascunho/vendido/cancelado)
|
||||||
|
* - slot padrão para conteúdo
|
||||||
|
* - slot opcional para ações
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: "EliCartao",
|
||||||
|
components: { EliBadge },
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
/** Título de fallback caso o slot `titulo` não seja usado. */
|
||||||
|
titulo: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status semântico do cartão.
|
||||||
|
* Usado para cor/label e para permitir filtros por status.
|
||||||
|
*/
|
||||||
|
status: {
|
||||||
|
type: String as PropType<CartaoStatus>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Variante visual do v-card (Vuetify). */
|
||||||
|
variant: {
|
||||||
|
type: String as PropType<CartaoVariante>,
|
||||||
|
default: "outlined",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
/** Emit opcional para padronizar clique no cartão. */
|
||||||
|
clicar: (_status: CartaoStatus) => true,
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const rotuloStatus = computed(() => {
|
||||||
|
// Mantém label em PT-BR (não abreviar)
|
||||||
|
return props.status;
|
||||||
|
});
|
||||||
|
|
||||||
|
const corStatus = computed(() => {
|
||||||
|
// Cor neutra por padrão e semântica por status
|
||||||
|
switch (props.status) {
|
||||||
|
case "novo":
|
||||||
|
return "primary";
|
||||||
|
case "rascunho":
|
||||||
|
return "secondary";
|
||||||
|
case "vendido":
|
||||||
|
return "success";
|
||||||
|
case "cancelado":
|
||||||
|
return "error";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const classeStatus = computed(() => `eli-cartao--${props.status}`);
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
emit("clicar", props.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rotuloStatus,
|
||||||
|
corStatus,
|
||||||
|
classeStatus,
|
||||||
|
onClick,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.eli-cartao {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eli-cartao__titulo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eli-cartao__titulo-texto {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eli-cartao__conteudo {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eli-cartao__acoes {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hooks visuais por status (sem forçar cor, deixa o tema do Vuetify fazer o trabalho) */
|
||||||
|
.eli-cartao--cancelado {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
71
src/componentes/cartao/README.md
Normal file
71
src/componentes/cartao/README.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# EliCartao
|
||||||
|
|
||||||
|
O `EliCartao` é um componente de cartão focado em **listas/pipelines** (ex.: oportunidades e propostas comerciais), com um **status semântico** padronizado.
|
||||||
|
|
||||||
|
Ele encapsula o `v-card` (Vuetify) e adiciona:
|
||||||
|
|
||||||
|
- Área de **título**
|
||||||
|
- **Badge de status** padronizado (`novo`, `rascunho`, `vendido`, `cancelado`)
|
||||||
|
- Slot de **conteúdo**
|
||||||
|
- Slot opcional de **ações**
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| Prop | Tipo | Padrão | Descrição |
|
||||||
|
|----------|--------------------------------------------------|--------------|-----------|
|
||||||
|
| `titulo` | `string` | `""` | Título de fallback (se o slot `titulo` não for usado). |
|
||||||
|
| `status` | `"novo" \| "rascunho" \| "vendido" \| "cancelado"` | *(obrigatório)* | Status semântico do cartão. Controla label/cor do badge e classes. |
|
||||||
|
| `variant`| `"outlined" \| "flat" \| "elevated" \| "tonal"` | `"outlined"` | Variante do `v-card` (Vuetify). |
|
||||||
|
|
||||||
|
### Emits
|
||||||
|
|
||||||
|
| Evento | Payload | Quando dispara |
|
||||||
|
|----------|-------------------------|----------------|
|
||||||
|
| `clicar` | `(status: CartaoStatus)`| Quando o cartão for clicado (opcional; útil para padronizar navegação). |
|
||||||
|
|
||||||
|
> Observação: o componente deixa `v-bind="$attrs"` para você passar `class`, `style`, `to`, `href`, etc.
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
| Slot | Objetivo |
|
||||||
|
|----------|----------|
|
||||||
|
| `titulo` | Conteúdo customizado do título (override do `titulo`). |
|
||||||
|
| `default`| Corpo do cartão. |
|
||||||
|
| `acoes` | Rodapé de ações (botões/links). |
|
||||||
|
|
||||||
|
## Exemplos
|
||||||
|
|
||||||
|
### 1) Cartão simples
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<EliCartao titulo="Proposta #1024" status="novo">
|
||||||
|
<div><strong>Cliente:</strong> ACME Ltda</div>
|
||||||
|
<div><strong>Valor:</strong> R$ 12.500,00</div>
|
||||||
|
</EliCartao>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) Título via slot + ações
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<EliCartao status="vendido">
|
||||||
|
<template #titulo>
|
||||||
|
<span>Oportunidade #204</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div>Cliente: Empresa X</div>
|
||||||
|
|
||||||
|
<template #acoes>
|
||||||
|
<EliBotao variant="text">Abrir</EliBotao>
|
||||||
|
<EliBotao variant="text" color="secondary">Editar</EliBotao>
|
||||||
|
</template>
|
||||||
|
</EliCartao>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Casos de borda / comportamento esperado
|
||||||
|
|
||||||
|
- `status` é obrigatório e sempre deve ser um valor suportado.
|
||||||
|
- Status `cancelado` aplica leve redução de opacidade via classe CSS.
|
||||||
|
- A renderização de ações só ocorre se o slot `acoes` existir.
|
||||||
|
|
||||||
2
src/componentes/cartao/index.ts
Normal file
2
src/componentes/cartao/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as EliCartao } from "./EliCartao.vue";
|
||||||
|
|
||||||
|
|
@ -3,11 +3,13 @@ import { EliOlaMundo } from "./componentes/ola_mundo";
|
||||||
import { EliBotao } from "./componentes/botao";
|
import { EliBotao } from "./componentes/botao";
|
||||||
import { EliBadge } from "./componentes/indicador";
|
import { EliBadge } from "./componentes/indicador";
|
||||||
import { EliInput } from "./componentes/campo";
|
import { EliInput } from "./componentes/campo";
|
||||||
|
import { EliCartao } from "./componentes/cartao";
|
||||||
|
|
||||||
export { EliOlaMundo };
|
export { EliOlaMundo };
|
||||||
export { EliBotao };
|
export { EliBotao };
|
||||||
export { EliBadge };
|
export { EliBadge };
|
||||||
export { EliInput };
|
export { EliInput };
|
||||||
|
export { EliCartao };
|
||||||
|
|
||||||
const EliVue: Plugin = {
|
const EliVue: Plugin = {
|
||||||
install(app: App) {
|
install(app: App) {
|
||||||
|
|
@ -15,6 +17,7 @@ const EliVue: Plugin = {
|
||||||
app.component("EliBotao", EliBotao);
|
app.component("EliBotao", EliBotao);
|
||||||
app.component("EliBadge", EliBadge);
|
app.component("EliBadge", EliBadge);
|
||||||
app.component("EliInput", EliInput);
|
app.component("EliInput", EliInput);
|
||||||
|
app.component("EliCartao", EliCartao);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<v-tabs v-model="aba" color="primary" density="comfortable">
|
<v-tabs v-model="aba" color="primary" density="comfortable">
|
||||||
<v-tab value="botao">Botão</v-tab>
|
<v-tab value="botao">Botão</v-tab>
|
||||||
<v-tab value="indicador">Indicador</v-tab>
|
<v-tab value="indicador">Indicador</v-tab>
|
||||||
|
<v-tab value="cartao">Cartão</v-tab>
|
||||||
<v-tab value="campo">Campo</v-tab>
|
<v-tab value="campo">Campo</v-tab>
|
||||||
<v-tab value="ola_mundo">Demo</v-tab>
|
<v-tab value="ola_mundo">Demo</v-tab>
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
|
|
@ -13,6 +14,7 @@
|
||||||
|
|
||||||
<BotaoPlayground v-if="aba === 'botao'" />
|
<BotaoPlayground v-if="aba === 'botao'" />
|
||||||
<IndicadorPlayground v-else-if="aba === 'indicador'" />
|
<IndicadorPlayground v-else-if="aba === 'indicador'" />
|
||||||
|
<CartaoPlayground v-else-if="aba === 'cartao'" />
|
||||||
<CampoPlayground v-else-if="aba === 'campo'" />
|
<CampoPlayground v-else-if="aba === 'campo'" />
|
||||||
<OlaMundoPlayground v-else />
|
<OlaMundoPlayground v-else />
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
@ -22,6 +24,7 @@
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import BotaoPlayground from "./botao.playground.vue";
|
import BotaoPlayground from "./botao.playground.vue";
|
||||||
import IndicadorPlayground from "./indicador.playground.vue";
|
import IndicadorPlayground from "./indicador.playground.vue";
|
||||||
|
import CartaoPlayground from "./cartao.playground.vue";
|
||||||
import CampoPlayground from "./campo.playground.vue";
|
import CampoPlayground from "./campo.playground.vue";
|
||||||
import OlaMundoPlayground from "./ola_mundo.playground.vue";
|
import OlaMundoPlayground from "./ola_mundo.playground.vue";
|
||||||
|
|
||||||
|
|
@ -30,12 +33,13 @@ export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
BotaoPlayground,
|
BotaoPlayground,
|
||||||
IndicadorPlayground,
|
IndicadorPlayground,
|
||||||
|
CartaoPlayground,
|
||||||
CampoPlayground,
|
CampoPlayground,
|
||||||
OlaMundoPlayground,
|
OlaMundoPlayground,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
aba: "botao" as "botao" | "indicador" | "campo" | "ola_mundo",
|
aba: "botao" as "botao" | "indicador" | "cartao" | "campo" | "ola_mundo",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
202
src/playground/cartao.playground.vue
Normal file
202
src/playground/cartao.playground.vue
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
<template>
|
||||||
|
<section class="stack">
|
||||||
|
<h2>EliCartao (pipeline)</h2>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<EliInput
|
||||||
|
v-model="filtro"
|
||||||
|
label="Buscar"
|
||||||
|
placeholder="Cliente, título, valor..."
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EliBotao @click="criar">Novo card</EliBotao>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="colunas">
|
||||||
|
<section v-for="coluna in colunas" :key="coluna.status" class="coluna">
|
||||||
|
<header class="coluna-header">
|
||||||
|
<strong class="text-subtitle-1">{{ coluna.titulo }}</strong>
|
||||||
|
<EliBadge :badge="coluna.itens.length" radius="pill" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<EliCartao
|
||||||
|
v-for="item in itensFiltrados(coluna.itens)"
|
||||||
|
:key="item.id"
|
||||||
|
:titulo="item.titulo"
|
||||||
|
:status="coluna.status"
|
||||||
|
>
|
||||||
|
<div class="linha"><strong>Cliente:</strong> {{ item.cliente }}</div>
|
||||||
|
<div class="linha"><strong>Valor:</strong> {{ item.valor }}</div>
|
||||||
|
<div class="linha"><strong>Vencimento:</strong> {{ item.vencimento }}</div>
|
||||||
|
|
||||||
|
<template #acoes>
|
||||||
|
<EliBotao variant="text" @click="abrir(item)">Abrir</EliBotao>
|
||||||
|
<EliBotao variant="text" color="secondary" @click="editar(item)">
|
||||||
|
Editar
|
||||||
|
</EliBotao>
|
||||||
|
</template>
|
||||||
|
</EliCartao>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from "vue";
|
||||||
|
import { EliBadge } from "@/componentes/indicador";
|
||||||
|
import { EliBotao } from "@/componentes/botao";
|
||||||
|
import { EliInput } from "@/componentes/campo";
|
||||||
|
import { EliCartao } from "@/componentes/cartao";
|
||||||
|
import type { CartaoStatus } from "@/tipos";
|
||||||
|
|
||||||
|
type Card = {
|
||||||
|
id: string;
|
||||||
|
titulo: string;
|
||||||
|
cliente: string;
|
||||||
|
valor: string;
|
||||||
|
vencimento: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Coluna = {
|
||||||
|
status: CartaoStatus;
|
||||||
|
titulo: string;
|
||||||
|
itens: Card[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "CartaoPlayground",
|
||||||
|
components: { EliBadge, EliBotao, EliInput, EliCartao },
|
||||||
|
setup() {
|
||||||
|
const filtro = ref("");
|
||||||
|
|
||||||
|
const colunas = ref<Coluna[]>([
|
||||||
|
{
|
||||||
|
status: "novo",
|
||||||
|
titulo: "Novo",
|
||||||
|
itens: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
titulo: "Proposta #1024",
|
||||||
|
cliente: "ACME Ltda",
|
||||||
|
valor: "R$ 12.500,00",
|
||||||
|
vencimento: "10/01/2026",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: "rascunho",
|
||||||
|
titulo: "Rascunho",
|
||||||
|
itens: [
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
titulo: "Oportunidade #204",
|
||||||
|
cliente: "Empresa X",
|
||||||
|
valor: "R$ 8.000,00",
|
||||||
|
vencimento: "15/01/2026",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: "vendido",
|
||||||
|
titulo: "Vendido",
|
||||||
|
itens: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: "cancelado",
|
||||||
|
titulo: "Cancelado",
|
||||||
|
itens: [
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
titulo: "Proposta #999",
|
||||||
|
cliente: "Cliente Y",
|
||||||
|
valor: "R$ 3.200,00",
|
||||||
|
vencimento: "02/01/2026",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
function itensFiltrados(itens: Card[]) {
|
||||||
|
const q = filtro.value.trim().toLowerCase();
|
||||||
|
if (!q) return itens;
|
||||||
|
return itens.filter((i) =>
|
||||||
|
`${i.titulo} ${i.cliente} ${i.valor}`.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function criar() {
|
||||||
|
// Exemplo: criar card no status "novo"
|
||||||
|
colunas.value[0]?.itens.unshift({
|
||||||
|
id: String(Date.now()),
|
||||||
|
titulo: "Nova oportunidade",
|
||||||
|
cliente: "(definir)",
|
||||||
|
valor: "R$ 0,00",
|
||||||
|
vencimento: "--/--/----",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function abrir(_item: Card) {
|
||||||
|
// Navegar para detalhes
|
||||||
|
}
|
||||||
|
|
||||||
|
function editar(_item: Card) {
|
||||||
|
// Abrir modal/rota de edição
|
||||||
|
}
|
||||||
|
|
||||||
|
return { filtro, colunas, itensFiltrados, criar, abrir, editar };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
grid-template-columns: minmax(260px, 420px) auto;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colunas {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.coluna {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coluna-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linha {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.toolbar {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
6
src/tipos/cartao.ts
Normal file
6
src/tipos/cartao.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
/**
|
||||||
|
* Tipos do componente EliCartao.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type CartaoStatus = "novo" | "rascunho" | "vendido" | "cancelado";
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export * from "./botao";
|
export * from "./botao";
|
||||||
|
export * from "./cartao";
|
||||||
export * from "./campo";
|
export * from "./campo";
|
||||||
export * from "./indicador";
|
export * from "./indicador";
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue