feat(#29): Arcade Neon design system — visual overhaul completo
Implementa o sistema visual Arcade Neon do neon-handoff/ em todo o app: - tokens.css: tokens --fc-*, utilities (fc-pixel/mono/body/glow), form inputs/buttons globais - AppHud.vue: HUD persistente com LV/XP/META + nav chips por rota - BottomNav.vue: nav mobile com 5 rotas - NeonPanel.vue, CharacterSprite.vue, XPBar.vue, HUDStat.vue: componentes base - MonthSwitcher.vue, LevelUpModal.vue - HomeView, CharacterView, TransactionsView, ImportView, CategoriesView, AccountsView, SettingsView: reescritos com Arcade Neon - sprites PNG (lv01–lv15) + sprite-data.json: cosméticos com temas de cor - game.ts: equippedTheme getter para merge de cores do cosmético equipado - 008_cosmetics.sql: seeds alinhados com sprite-data.json Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useAccountsStore, type AccountInput } from '@/stores/accounts'
|
||||
import NeonPanel from '@/components/NeonPanel.vue'
|
||||
|
||||
const store = useAccountsStore()
|
||||
|
||||
onMounted(() => store.fetchAll())
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
@@ -64,93 +64,188 @@ async function remove(id: number, name: string) {
|
||||
function fmt(v: number) {
|
||||
return v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
|
||||
}
|
||||
|
||||
function totalBalance() {
|
||||
return store.accounts.reduce((s, a) => s + a.balance, 0)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<h1>Contas Bancárias</h1>
|
||||
<div class="fc-view">
|
||||
<span class="fc-pixel fc-view__title">:: CONTAS BANCÁRIAS</span>
|
||||
|
||||
<form class="form-card" @submit.prevent="submit">
|
||||
<h2>{{ editId !== null ? 'Editar conta' : 'Nova conta' }}</h2>
|
||||
<div class="form-row">
|
||||
<input v-model="form.name" placeholder="Ex: Nubank Corrente" required class="input-name" />
|
||||
<select v-model="form.type" class="input-sm">
|
||||
<option value="checking">Conta Corrente</option>
|
||||
<option value="savings">Poupança</option>
|
||||
<option value="investment">Investimento</option>
|
||||
<option value="credit">Cartão de Crédito</option>
|
||||
</select>
|
||||
<input v-model="initialRaw" placeholder="Saldo inicial (ex: 1.500,00)" class="input-sm input-amount" />
|
||||
<!-- Form -->
|
||||
<NeonPanel :title="editId !== null ? 'EDITAR CONTA' : 'NOVA CONTA'">
|
||||
<form class="fc-acc-form" @submit.prevent="submit">
|
||||
<div class="fc-acc-form__row">
|
||||
<input v-model="form.name" placeholder="Ex: Nubank Corrente" required class="fc-input fc-acc-form__name" />
|
||||
<select v-model="form.type" class="fc-select fc-acc-form__type">
|
||||
<option value="checking">Conta Corrente</option>
|
||||
<option value="savings">Poupança</option>
|
||||
<option value="investment">Investimento</option>
|
||||
<option value="credit">Cartão de Crédito</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="initialRaw"
|
||||
placeholder="Saldo inicial (ex: 1.500,00)"
|
||||
class="fc-input fc-acc-form__balance"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="formError" class="fc-acc-form__error fc-mono">{{ formError }}</p>
|
||||
<div class="fc-acc-form__actions">
|
||||
<button type="submit" class="fc-btn fc-btn--primary">{{ editId !== null ? 'SALVAR' : 'ADICIONAR' }}</button>
|
||||
<button v-if="editId !== null" type="button" class="fc-btn fc-btn--ghost" @click="cancelEdit">CANCELAR</button>
|
||||
</div>
|
||||
</form>
|
||||
</NeonPanel>
|
||||
|
||||
<!-- Summary -->
|
||||
<NeonPanel v-if="store.accounts.length > 0" title="PATRIMÔNIO TOTAL">
|
||||
<div class="fc-acc-total">
|
||||
<span class="fc-mono fc-acc-total__value" :class="totalBalance() >= 0 ? 'fc-text-green' : 'fc-text-red'">
|
||||
{{ fmt(totalBalance()) }}
|
||||
</span>
|
||||
<span class="fc-mono fc-acc-total__sub">{{ store.accounts.length }} conta{{ store.accounts.length !== 1 ? 's' : '' }}</span>
|
||||
</div>
|
||||
<p v-if="formError" class="form-error">{{ formError }}</p>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ editId !== null ? 'Salvar' : 'Adicionar' }}</button>
|
||||
<button v-if="editId !== null" type="button" class="btn btn-ghost" @click="cancelEdit">Cancelar</button>
|
||||
</div>
|
||||
</form>
|
||||
</NeonPanel>
|
||||
|
||||
<p v-if="store.loading">Carregando…</p>
|
||||
|
||||
<div v-else>
|
||||
<div class="total-bar" v-if="store.accounts.length > 0">
|
||||
<span>Patrimônio total</span>
|
||||
<strong>{{ fmt(store.accounts.reduce((s, a) => s + a.balance, 0)) }}</strong>
|
||||
</div>
|
||||
|
||||
<ul class="account-list">
|
||||
<!-- Account list -->
|
||||
<NeonPanel title="CONTAS">
|
||||
<p v-if="store.loading" class="fc-acc-loading fc-mono">carregando...</p>
|
||||
<ul v-else class="fc-acc-list">
|
||||
<li
|
||||
v-for="a in store.accounts"
|
||||
:key="a.id"
|
||||
class="account-item"
|
||||
:class="{ editing: editId === a.id }"
|
||||
class="fc-acc-item"
|
||||
:class="{ 'fc-acc-item--editing': editId === a.id }"
|
||||
>
|
||||
<div class="account-info">
|
||||
<strong>{{ a.name }}</strong>
|
||||
<span class="account-meta">{{ typeLabels[a.type] }} · Saldo inicial {{ fmt(a.initial_balance) }}</span>
|
||||
<div class="fc-acc-item__info">
|
||||
<span class="fc-body fc-acc-item__name">{{ a.name }}</span>
|
||||
<span class="fc-mono fc-acc-item__meta">
|
||||
{{ typeLabels[a.type] }} · inicial {{ fmt(a.initial_balance) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="account-right">
|
||||
<span class="account-balance" :class="a.balance >= 0 ? 'positive' : 'negative'">
|
||||
<div class="fc-acc-item__right">
|
||||
<span class="fc-mono fc-acc-item__balance" :class="a.balance >= 0 ? 'fc-text-green' : 'fc-text-red'">
|
||||
{{ fmt(a.balance) }}
|
||||
</span>
|
||||
<div class="item-actions">
|
||||
<button class="btn btn-sm" @click="startEdit(a.id)">Editar</button>
|
||||
<button class="btn btn-sm btn-danger" @click="remove(a.id, a.name)">Excluir</button>
|
||||
<div class="fc-acc-item__actions">
|
||||
<button class="fc-btn fc-btn--sm fc-btn--ghost" @click="startEdit(a.id)">EDITAR</button>
|
||||
<button class="fc-btn fc-btn--sm fc-btn--danger" @click="remove(a.id, a.name)">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="store.accounts.length === 0" class="empty">Nenhuma conta cadastrada</li>
|
||||
<li v-if="store.accounts.length === 0" class="fc-acc-empty fc-mono">— nenhuma conta cadastrada —</li>
|
||||
</ul>
|
||||
</div>
|
||||
</NeonPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page { max-width: 640px; margin: 0 auto; padding: 1.5rem 1rem; font-family: sans-serif; }
|
||||
h1 { font-size: 1.5rem; margin-bottom: 1.25rem; }
|
||||
h2 { font-size: 0.95rem; font-weight: 600; margin: 0 0 0.75rem; }
|
||||
.form-card { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; }
|
||||
.form-row { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
|
||||
.input-name { flex: 1; min-width: 160px; padding: 0.45rem 0.6rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.875rem; }
|
||||
.input-sm { padding: 0.45rem 0.6rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.875rem; }
|
||||
.input-amount { width: 140px; }
|
||||
.form-error { color: #dc2626; font-size: 0.875rem; margin: 0.5rem 0 0; }
|
||||
.form-actions { margin-top: 0.75rem; display: flex; gap: 0.5rem; }
|
||||
.btn { padding: 0.4rem 1rem; border: 1px solid #d1d5db; border-radius: 6px; cursor: pointer; font-size: 0.875rem; background: #fff; }
|
||||
.btn-primary { background: #4f46e5; color: #fff; border-color: #4f46e5; }
|
||||
.btn-ghost { background: transparent; }
|
||||
.btn-sm { padding: 0.25rem 0.6rem; }
|
||||
.btn-danger { color: #dc2626; border-color: #fca5a5; }
|
||||
.total-bar { display: flex; justify-content: space-between; align-items: center; background: #1e293b; color: #f8fafc; border-radius: 8px; padding: 0.75rem 1rem; margin-bottom: 0.75rem; font-size: 0.9rem; }
|
||||
.total-bar strong { font-size: 1.1rem; }
|
||||
.account-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.account-item { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; border: 1px solid #e5e7eb; border-radius: 8px; background: #fff; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.account-item.editing { border-color: #4f46e5; box-shadow: 0 0 0 2px #e0e7ff; }
|
||||
.account-info { display: flex; flex-direction: column; gap: 0.2rem; }
|
||||
.account-meta { font-size: 0.8rem; color: #6b7280; }
|
||||
.account-right { display: flex; align-items: center; gap: 1rem; }
|
||||
.account-balance { font-size: 1.1rem; font-weight: 700; }
|
||||
.positive { color: #059669; }
|
||||
.negative { color: #dc2626; }
|
||||
.item-actions { display: flex; gap: 0.4rem; }
|
||||
.empty { color: #9ca3af; text-align: center; padding: 2rem; }
|
||||
.fc-view {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding: var(--fc-space-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--fc-space-4);
|
||||
padding-bottom: 96px;
|
||||
}
|
||||
|
||||
.fc-view__title {
|
||||
font-size: 11px;
|
||||
color: var(--fc-accent-2);
|
||||
}
|
||||
|
||||
.fc-acc-form__row {
|
||||
display: flex;
|
||||
gap: var(--fc-space-2);
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.fc-acc-form__name { flex: 1; min-width: 160px; }
|
||||
.fc-acc-form__type { width: 160px; }
|
||||
.fc-acc-form__balance { width: 160px; }
|
||||
|
||||
.fc-acc-form__error {
|
||||
color: var(--fc-red);
|
||||
font-size: 11px;
|
||||
margin-top: var(--fc-space-2);
|
||||
}
|
||||
|
||||
.fc-acc-form__actions {
|
||||
display: flex;
|
||||
gap: var(--fc-space-2);
|
||||
margin-top: var(--fc-space-3);
|
||||
}
|
||||
|
||||
/* Total */
|
||||
.fc-acc-total {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--fc-space-3);
|
||||
}
|
||||
|
||||
.fc-acc-total__value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.fc-acc-total__sub {
|
||||
font-size: 11px;
|
||||
color: var(--fc-text-dim);
|
||||
}
|
||||
|
||||
/* List */
|
||||
.fc-acc-loading, .fc-acc-empty {
|
||||
font-size: 11px;
|
||||
color: var(--fc-text-dim);
|
||||
text-align: center;
|
||||
padding: var(--fc-space-4) 0;
|
||||
}
|
||||
|
||||
.fc-acc-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--fc-space-2);
|
||||
}
|
||||
|
||||
.fc-acc-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--fc-space-3);
|
||||
padding: 12px var(--fc-space-3);
|
||||
border: 1px solid var(--fc-panel-edge);
|
||||
border-radius: var(--fc-radius);
|
||||
flex-wrap: wrap;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
|
||||
.fc-acc-item--editing {
|
||||
border-color: var(--fc-accent-3);
|
||||
box-shadow: 0 0 8px rgba(168,85,247,.3);
|
||||
}
|
||||
|
||||
.fc-acc-item__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.fc-acc-item__name { font-size: 14px; font-weight: 500; }
|
||||
.fc-acc-item__meta { font-size: 11px; color: var(--fc-text-dim); }
|
||||
|
||||
.fc-acc-item__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--fc-space-3);
|
||||
}
|
||||
|
||||
.fc-acc-item__balance { font-size: 16px; font-weight: 700; }
|
||||
.fc-acc-item__actions { display: flex; gap: var(--fc-space-1); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user