- Tabela `profiles` + coluna `profile_id` em todas as entidades (categories, transactions, recurring_expenses, accounts, player_profile, xp_events, player_quests, player_achievements, player_cosmetics) - Dados existentes migrados para profile_id = 1 (Manoel) - CLI `./api create-user --name <n> --password <p>` cria perfil com seed de categorias e player_profile; faz upsert de senha se já existir - Auth substituída: cookie+APP_PASSWORD → JWT Bearer 24h (HS256) - Middleware RequireAuth injeta profile_id no context de todas as rotas - Todos os repositórios filtram por profile_id do context - Endpoints: POST /api/auth/login, GET /api/auth/me, POST /api/auth/change-password, POST /api/logout - Frontend: auth store usa localStorage (fc_token/fc_profile), api.ts envia Authorization header, LoginView usa campo name - SettingsView reescrita com troca de senha e logout - docker-compose.yml: remove APP_USERNAME/APP_PASSWORD, adiciona JWT_SECRET Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
21 KiB
Financeiro Carvalho · Style Guide
Direção: Arcade Neon Stack alvo: Vue 3 + Vite + TypeScript + Pinia Princípio: finanças vestindo roupa de fliperama. Fundo escuro, glow neon nos números que importam, fonte pixel nos labels e mono nos dados. HUD sempre presente.
1. Tokens
Copie tokens.css na raiz do src/styles/ e importe em main.ts. Todas as variáveis CSS abaixo são o contrato — componentes consomem var(--fc-*), nunca hex direto.
1.1 Cores
| Token | Valor | Uso |
|---|---|---|
--fc-bg |
#0a0418 |
Fundo base do app |
--fc-bg-raised |
#150827 |
Card / panel topo do gradiente |
--fc-bg-panel |
#1c0d33 |
Card / panel base do gradiente |
--fc-panel-edge |
#2d1857 |
Borda padrão de painéis |
--fc-line |
#3a1f6e |
Grid de fundo, dividers internos |
--fc-text |
#f4eaff |
Texto principal (off-white lilás) |
--fc-text-dim |
#8b75b8 |
Texto secundário, labels |
--fc-accent |
#ff2d95 |
Magenta · destaques quentes, alertas suaves, "diária" |
--fc-accent-2 |
#00f0ff |
Ciano · links, navegação, "semanal" |
--fc-accent-3 |
#a855f7 |
Roxo · CTA padrão, painéis com glow |
--fc-gold |
#ffd84d |
XP, conquistas, "mensal" |
--fc-green |
#39ff7a |
Meta atingida, valores positivos |
--fc-red |
#ff3b6b |
Meta falhou, alerta crítico, recorrência ausente |
Regra de uso:
- Magenta é a cor do "agora" — botões primários secundários, status do dia.
- Ciano é a cor da informação fria — XP, links, navegação ativa.
- Roxo é estrutura — painel em destaque, hover, foco. Quase sempre tem
box-shadow: 0 0 24px rgba(168,85,247,.25). - Dourado é recompensa — qualquer coisa relacionada a XP, nível, conquista.
- Verde/vermelho ficam reservados pro binário "meta sim/meta não".
1.2 Tipografia
Importe via Google Fonts no <head>:
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
| Família | Uso | Tamanhos comuns |
|---|---|---|
Press Start 2P |
Labels, títulos de painel, números heroicos (47%), nav | 7px, 8px, 9px, 14px, 22px, 40px, 56px |
JetBrains Mono |
Valores numéricos (R$), datas, IDs | 10px, 11px, 12px, 16px |
Inter |
Fallback para corpo de texto longo (descrições de quest, etc.) | 13px, 14px |
Classes utilitárias:
.fc-pixel { font-family: 'Press Start 2P', monospace; letter-spacing: .02em; }
.fc-mono { font-family: 'JetBrains Mono', monospace; }
.fc-body { font-family: 'Inter', system-ui, sans-serif; }
Regra: nunca usar Inter pra número. Nunca usar Press Start 2P pra parágrafo (>2 linhas). Press Start 2P em maiúscula sempre.
1.3 Tamanhos e espaçamento
| Token | Valor | Uso |
|---|---|---|
--fc-radius-sm |
2px |
Botões, chips |
--fc-radius |
4px |
Painéis |
--fc-space-1 |
4px |
Gap minúsculo |
--fc-space-2 |
8px |
Gap entre chips |
--fc-space-3 |
12px |
Padding interno de card pequeno |
--fc-space-4 |
16px |
Padding/gap padrão |
--fc-space-5 |
24px |
Padding de painel grande |
Página tem padding: 24px nas bordas. Grids usam gap: 16px entre cards.
1.4 Sombras e glow
Sombra padrão de painel:
box-shadow:
inset 0 1px 0 rgba(255,255,255,.05),
0 0 0 1px rgba(0,0,0,.4),
0 8px 24px rgba(0,0,0,.4);
Painel com glow (destaque do hero):
box-shadow:
inset 0 1px 0 rgba(255,255,255,.05),
0 0 0 1px var(--fc-accent-3),
0 0 24px rgba(168,85,247,.25),
0 8px 24px rgba(0,0,0,.5);
border-color: var(--fc-accent-3);
Glow em texto (números heroicos):
text-shadow: 0 0 8px rgba(var-rgb, .53);
/* ex: text-shadow: 0 0 8px #39ff7a88; */
Glow em ícone/cor:
box-shadow: 0 0 8px <cor>88;
Sempre que o app exibir um número grande de "vitória", ele recebe um glow do tom correspondente. Use com parcimônia — só nos pontos focais.
2. Layout
2.1 Estrutura geral
Toda tela do app tem essa estrutura vertical:
┌─────────────────────────────────────────┐
│ HUD (Top Bar) │ ← persistente, navegação + stats RPG
├─────────────────────────────────────────┤
│ Sub-header (título da rota + ações) │
├─────────────────────────────────────────┤
│ Conteúdo │ ← grid de painéis
│ │
└─────────────────────────────────────────┘
HUD é elemento de identidade — não dispensar em nenhuma rota.
2.2 Grid de painéis
Dashboard usa grid 1.6fr 1fr (coluna principal mais larga que a lateral). Cards empilhados com gap: 16px.
Personagem usa grid 1fr 1.4fr (personagem à esquerda, quests/conquistas à direita).
2.3 Fundo do app
O fundo tem 3 camadas (em ordem, do mais ao fundo):
- Cor sólida
#0a0418 - Dois gradientes radiais sutis (roxo no topo, magenta no canto inferior direito)
- Grid pontilhado fino (32×32px linhas, opacidade 12%) sobre tudo
- Scanlines horizontais finas (CRT vibe) com opacidade 2,5%
CSS do body/root do app:
.fc-app {
background:
radial-gradient(ellipse 80% 50% at 50% 0%, rgba(168,85,247,.18), transparent 70%),
radial-gradient(ellipse 60% 40% at 80% 100%, rgba(255,45,149,.12), transparent 60%),
var(--fc-bg);
position: relative;
min-height: 100vh;
}
.fc-app::before { /* scanlines */
content: '';
position: absolute; inset: 0;
background-image: repeating-linear-gradient(0deg, rgba(255,255,255,.025) 0 1px, transparent 1px 3px);
pointer-events: none;
z-index: 2;
}
.fc-app::after { /* grid */
content: '';
position: absolute; inset: 0;
background-image:
linear-gradient(var(--fc-line) 1px, transparent 1px),
linear-gradient(90deg, var(--fc-line) 1px, transparent 1px);
background-size: 32px 32px;
opacity: .12;
pointer-events: none;
}
.fc-app > * { position: relative; z-index: 1; }
3. Componentes
3.1 Panel
Wrapper para qualquer conteúdo. Tem cantos em "L" (corner brackets) opcionais — a estética HUD do Arcade Neon.
<!-- NeonPanel.vue -->
<template>
<div class="fc-panel" :class="{ 'fc-panel--glow': glow, [`fc-panel--${variant}`]: variant }">
<span v-if="corners" class="fc-corner fc-corner--tl" />
<span v-if="corners" class="fc-corner fc-corner--tr" />
<span v-if="corners" class="fc-corner fc-corner--bl" />
<span v-if="corners" class="fc-corner fc-corner--br" />
<slot />
</div>
</template>
<script setup lang="ts">
defineProps<{
glow?: boolean
corners?: boolean
variant?: 'danger' | 'success'
}>()
</script>
CSS:
.fc-panel {
position: relative;
background: linear-gradient(180deg, var(--fc-bg-raised), var(--fc-bg-panel));
border: 2px solid var(--fc-panel-edge);
border-radius: var(--fc-radius);
padding: 16px;
box-shadow:
inset 0 1px 0 rgba(255,255,255,.05),
0 0 0 1px rgba(0,0,0,.4),
0 8px 24px rgba(0,0,0,.4);
}
.fc-panel--glow {
border-color: var(--fc-accent-3);
box-shadow:
inset 0 1px 0 rgba(255,255,255,.05),
0 0 0 1px var(--fc-accent-3),
0 0 24px rgba(168,85,247,.25),
0 8px 24px rgba(0,0,0,.5);
}
.fc-panel--danger { border-color: var(--fc-red); box-shadow: 0 0 0 1px var(--fc-red), 0 0 16px rgba(255,59,107,.3); }
.fc-corner {
position: absolute; width: 8px; height: 8px;
border: 2px solid var(--fc-accent-2);
}
.fc-corner--tl { top: -2px; left: -2px; border-right: none; border-bottom: none; }
.fc-corner--tr { top: -2px; right: -2px; border-left: none; border-bottom: none; }
.fc-corner--bl { bottom: -2px; left: -2px; border-right: none; border-top: none; }
.fc-corner--br { bottom: -2px; right: -2px; border-left: none; border-top: none; }
Quando usar corners: somente no painel principal de cada coluna ou no hero. Eles são "decoração de HUD", não devem aparecer em cada card.
Quando usar glow: apenas no painel de status mais importante da rota — o de meta de poupança no dashboard, o card do personagem na tela RPG.
3.2 Título de painel
Todo painel começa com um título no padrão :: NOME:
<div class="fc-panel-title">:: GASTOS · CATEGORIA</div>
.fc-panel-title {
font-family: 'Press Start 2P', monospace;
font-size: 9px;
color: var(--fc-accent-2);
margin-bottom: 14px;
letter-spacing: .04em;
}
O :: prefix é assinatura visual da direção. Mantém.
3.3 Button
Botão padrão é "neon outline":
.fc-btn {
font-family: 'Press Start 2P', monospace;
font-size: 9px;
padding: 9px 12px;
background: var(--fc-bg-raised);
border: 2px solid var(--fc-accent-3);
color: var(--fc-text);
cursor: pointer;
border-radius: 2px;
letter-spacing: .05em;
text-transform: uppercase;
transition: all .12s;
}
.fc-btn:hover {
background: var(--fc-accent-3);
color: white;
box-shadow: 0 0 16px rgba(168,85,247,.6);
}
.fc-btn--pink { border-color: var(--fc-accent); }
.fc-btn--pink:hover { background: var(--fc-accent); box-shadow: 0 0 16px rgba(255,45,149,.6); }
.fc-btn--cyan { border-color: var(--fc-accent-2); color: var(--fc-accent-2); }
.fc-btn--cyan:hover { background: var(--fc-accent-2); color: var(--fc-bg); box-shadow: 0 0 16px rgba(0,240,255,.6); }
Hierarquia:
- Roxo (padrão): ação primária genérica.
- Magenta: ação destrutiva ou de "agora" (registrar transação, atingir quest).
- Ciano: ação secundária, ver detalhes.
3.4 Chip
.fc-chip {
display: inline-block;
font-family: 'Press Start 2P', monospace;
font-size: 7px;
padding: 4px 6px;
border-radius: 2px;
letter-spacing: .05em;
text-transform: uppercase;
}
Cores dependem do contexto:
- Quest "Diária" →
background: var(--fc-accent) - Quest "Semanal" →
background: var(--fc-accent-2) - Quest "Mensal" →
background: var(--fc-gold) - Categoria de transação →
color: var(--fc-text-dim)apenas (sem fundo)
3.5 Bar (XP, progresso de quest, gauge)
<template>
<div class="fc-bar" :class="{ 'fc-bar--success': success }">
<div class="fc-bar__fill" :style="{ width: pct + '%' }" />
</div>
</template>
<script setup lang="ts">
defineProps<{ pct: number; success?: boolean }>()
</script>
.fc-bar {
height: 14px;
background: var(--fc-bg);
border: 2px solid var(--fc-panel-edge);
position: relative;
overflow: hidden;
}
.fc-bar__fill {
height: 100%;
background: linear-gradient(90deg, var(--fc-accent-2), var(--fc-accent-3), var(--fc-accent));
position: relative;
}
.fc-bar__fill::after {
content:''; position: absolute; inset: 0;
background-image: repeating-linear-gradient(90deg, rgba(0,0,0,.2) 0 2px, transparent 2px 6px);
}
.fc-bar--success { border-color: var(--fc-green); }
.fc-bar--success .fc-bar__fill { background: linear-gradient(90deg, var(--fc-green), var(--fc-accent-2)); }
Tamanhos:
- XP geral (dashboard widget): 8px
- Quest mini: 6px
- Hero (poupado %): 14px
- Personagem (XP grande): 14px
3.6 HUD (top bar)
Persistente em todas as rotas. Contém: logo, separator, stats RPG (LV/XP/STREAK/META), nav.
Stats são essenciais — mesmo em telas que não são /personagem, o usuário tem que ver seu progresso o tempo todo. Esse é o ponto da direção.
<header class="fc-hud">
<div class="fc-hud__logo">
<span class="fc-hud__logo-mark" />
<div>
<div class="fc-pixel" style="font-size: 10px; color: var(--fc-accent-2)">CARVALHO</div>
<div class="fc-pixel" style="font-size: 7px; color: var(--fc-text-dim)">FIN.SYS v1.4</div>
</div>
</div>
<div class="fc-hud__sep" />
<div class="fc-hud__stats">
<HUDStat label="LV" :value="user.level" color="var(--fc-gold)" />
<HUDStat label="XP" :value="user.xp" :sub="`/${user.xpNext}`" color="var(--fc-accent-2)" />
<HUDStat label="STREAK" :value="`${user.streak}d`" color="var(--fc-accent)" />
<HUDStat label="META" value="47%" sub="/40%" color="var(--fc-green)" />
</div>
<nav class="fc-hud__nav">
<!-- chip-shaped buttons, one per route -->
</nav>
</header>
.fc-hud {
display: flex; align-items: center; gap: 16px;
padding: 12px 20px;
border-bottom: 2px solid var(--fc-panel-edge);
background: linear-gradient(180deg, var(--fc-bg-panel), var(--fc-bg));
position: relative; z-index: 3;
}
.fc-hud__logo-mark {
display: inline-block;
width: 28px; height: 28px;
background: var(--fc-accent);
box-shadow: 0 0 12px var(--fc-accent);
position: relative;
}
.fc-hud__logo-mark::before, .fc-hud__logo-mark::after { content: ''; position: absolute; }
.fc-hud__logo-mark::before { inset: 5px; background: var(--fc-bg); }
.fc-hud__logo-mark::after { top: 9px; left: 9px; width: 10px; height: 10px; background: var(--fc-accent-2); }
Nav items são chips com borda. Item ativo: fundo --fc-accent-3 + glow.
3.7 Link (texto)
.fc-link {
color: var(--fc-accent-2);
font-family: 'Press Start 2P', monospace;
font-size: 8px;
cursor: pointer;
text-decoration: none;
letter-spacing: .04em;
}
.fc-link:hover {
text-decoration: underline;
text-shadow: 0 0 8px var(--fc-accent-2);
}
Use sempre VER+, ABRIR >, REGISTRAR > — verbo curto + seta. Toda navegação textual usa esse padrão pra reforçar o "menu de jogo".
3.8 Alerta pulsante (recorrência ausente)
@keyframes fc-blink { 0%,49% { opacity: 1; } 50%,100% { opacity: 0.2; } }
.fc-blink { animation: fc-blink 1.2s steps(1) infinite; }
Use no triângulo ⚠ que precede o título do alerta. Pisca em vermelho.
4. Tabelas / listas
Padrão de tabela de transações:
[ data ][ descrição ......... ][ CATEGORIA ][ -R$ X,XX ]
mono10 mono11 ellipsis pixel7 dim mono11 right
- Linhas usam
border-bottom: 1px dashed var(--fc-panel-edge)(a última, não). - Padding vertical de 8px por linha.
- Valor positivo → cor
--fc-green. - Valor negativo → cor
--fc-text(não vermelho — o vermelho é só pra alerta crítico).
5. Pixel Sprite (Character)
O personagem é desenhado a partir de uma grade 17×24 de células. Cores são parametrizáveis por slot de cosmético.
5.1 Slots de cor
| Slot | Token CSS sugerido | Cosmético que altera |
|---|---|---|
outline |
--fc-sprite-outline (#10131c) |
nunca muda |
skin |
--fc-sprite-skin (#f3c79b) |
nunca muda |
skinShade |
--fc-sprite-skin-shade (#c98863) |
nunca muda |
hat |
--fc-sprite-hat |
cosmético hat |
band |
--fc-sprite-band |
faixa do chapéu |
shirt |
--fc-sprite-shirt |
cosmético shirt |
shirtShade |
--fc-sprite-shirt-shade |
shadow do shirt |
pants |
--fc-sprite-pants |
cosmético pants |
pantsShade |
--fc-sprite-pants-shade |
shadow do pants |
boots |
--fc-sprite-boots |
cosmético shoes |
5.2 Render (Vue)
A grade vem do arquivo sprites/sprite-data.json (incluído). Componente:
<!-- CharacterSprite.vue -->
<template>
<svg :width="cols * scale" :height="rows * scale"
:viewBox="`0 0 ${cols * scale} ${rows * scale}`"
shape-rendering="crispEdges"
:class="['fc-sprite', { 'fc-sprite--bob': bob, 'fc-sprite--celebrate': celebrate }]">
<rect v-for="px in pixels" :key="`${px.x}-${px.y}`"
:x="px.x * scale" :y="px.y * scale" :width="scale" :height="scale"
:fill="colors[px.k]" />
</svg>
</template>
<script setup lang="ts">
import spriteData from '@/assets/sprite-data.json'
import { computed } from 'vue'
const props = withDefaults(defineProps<{
scale?: number
theme?: Partial<Record<string,string>>
bob?: boolean
celebrate?: boolean
}>(), { scale: 4, bob: true })
const rows = spriteData.rows
const cols = spriteData.cols
const defaults: Record<string,string> = {
L: '#10131c', s: '#f3c79b', S: '#c98863', e: '#10131c', m: '#80484b',
h: '#ffd84d', w: '#fde68a',
c: '#00f0ff', C: '#0369a1',
p: '#7c5a2e', P: '#4f3a1e',
b: '#0f172a',
}
const colors = computed(() => ({ ...defaults, ...(props.theme || {}) }))
const pixels = computed(() => {
const out: { x: number, y: number, k: string }[] = []
spriteData.grid.forEach((row: string, y: number) => {
for (let x = 0; x < row.length; x++) {
const k = row[x]
if (k === '.' || !colors.value[k]) continue
out.push({ x, y, k })
}
})
return out
})
</script>
<style scoped>
.fc-sprite { image-rendering: pixelated; line-height: 0; }
@keyframes fc-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
.fc-sprite--bob { animation: fc-bob 2.2s steps(2) infinite; }
@keyframes fc-celebrate { 0%,100% { transform: translateY(0) rotate(0); } 25% { transform: translateY(-6px) rotate(-3deg); } 75% { transform: translateY(-6px) rotate(3deg); } }
.fc-sprite--celebrate { animation: fc-celebrate 1s ease-in-out infinite; }
</style>
5.3 Tamanhos canônicos
- Dashboard widget:
scale={3}→ 51×72px no SVG - Mobile inline:
scale={2}→ 34×48px - Personagem hero:
scale={6}→ 102×144px
Sempre escalas inteiras. Nunca esticar.
5.4 Cosméticos (data)
A loja de cosméticos persiste no banco. Cada cosmético tem:
type Cosmetic = {
id: string
name: string
slot: 'hat' | 'shirt' | 'pants' | 'shoes' | 'cape'
unlockLv: number // nível necessário
colors: Partial<Record<'h'|'w'|'c'|'C'|'p'|'P'|'b', string>>
}
O store (Pinia) tem um getter equippedTheme que faz o merge das cores dos itens equipados. Esse objeto vai como prop theme para <CharacterSprite>.
6. Animações
| Trigger | Comportamento |
|---|---|
| Idle (sempre) | Sprite bobinha verticalmente (fc-sprite--bob, 2.2s steps(2) infinite) |
| Quest completa | Sprite vira fc-sprite--celebrate por 3s, depois volta |
| Level up | Modal sobrepõe a tela: painel com glow magenta, "+1 LEVEL", som opcional |
| XP ganho | Bar enche com transição width 0.6s ease-out. Numero do XP no HUD soma com tween 1s |
| Hover em card | transform: translateY(-2px) + shadow um pouco mais profundo |
| Alerta de recorrência | Triângulo ⚠ pisca (fc-blink) |
Sem motion gratuito. Cada animação tem um significado.
7. Iconografia
Esta direção evita ícones de biblioteca (Lucide, etc.). Em vez disso usa:
- Símbolos unicode pequenos pra categorias:
⌂casa,✚saúde,◉mercado,⚓veleiro,▦board games,↦transporte,•outros. - Cantos em "L" (
fc-corner-*) pra emoldurar painéis especiais. - Triângulo
▲pra alerta, estrela✦ou★pra conquista, diamante◆pra equipado. - Setas
›‹>pra navegação textual.
Quando precisar de um ícone novo: SVG inline pixel-art (8×8 ou 12×12), nunca importar ícone vetorial moderno.
Os pixels de PixelHeart e PixelCoin estão em sprites/icons.json.
8. Voz e copywriting
- Labels em maiúscula com Press Start 2P, curtas.
:: GASTOS · CATEGORIA,:: TRANSAÇÕES,:: PERSONAGEM. - Botões em maiúscula, verbo + (seta opcional).
REGISTRAR >,VER +,ABRIR >,IMPORTAR EXTRATO. - Mensagens de sucesso em tom de jogo:
QUEST OK,META BATIDA,+200 XP. - Mensagens de aviso em tom direto:
RECORRÊNCIA AUSENTE,QUEST FAIL. - Nome de seção da vida real ganha "stage":
:: STAGE 05 ::antes do nome do mês na rota/.
Evite tom motivacional vazio ("você consegue!"). O jogo recompensa com XP, não com frase.
9. Responsivo
- Desktop ≥ 1024px: grid
1.6fr 1fr(dashboard) ou1fr 1.4fr(personagem). HUD horizontal completo. - Tablet 768–1023px: grid colapsa pra 1 coluna. HUD ainda horizontal mas nav vira menu hambúrguer (preserve os stats).
- Mobile <768px:
- HUD compacta. Apenas LV + XP-bar visíveis + streak.
- Nav vira bottom bar fixa, 4 itens: HOME / TX / ADD (+) / PERS.
- Painéis empilham com
gap: 12px, padding interno reduz pra 14px. - Hero "47%" ocupa toda a largura, centralizado.
10. Arquivos deste pacote
neon-handoff/
├── STYLE_GUIDE.md ← este arquivo
├── tokens.css ← variáveis CSS prontas pra importar
├── preview.html ← referência visual interativa
├── components/
│ ├── NeonPanel.vue
│ ├── XPBar.vue
│ ├── HUDStat.vue
│ └── CharacterSprite.vue
├── sprites/
│ ├── sprite-data.json ← grade raw 17×24 (chave por cor)
│ ├── icons.json ← heart, coin, achievement
│ ├── manoel-default.png ← 1× (17×24px) e 4× (68×96px)
│ ├── manoel-default-4x.png
│ ├── manoel-lv01.png
│ ├── manoel-lv03-bone-verolme.png
│ ├── manoel-lv05-wingspan.png
│ ├── manoel-lv07-chapeu-sol.png
│ ├── manoel-lv10-anti-vento.png
│ ├── manoel-lv15-capitao.png
│ └── manoel-sheet-4x.png ← todos os 7 lado a lado, pra sprite-sheet
└── README.md