feat: multi-usuário com autenticação JWT
- 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]>
This commit is contained in:
@@ -0,0 +1,640 @@
|
||||
# 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>`:
|
||||
|
||||
```html
|
||||
<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:
|
||||
|
||||
```css
|
||||
.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:
|
||||
```css
|
||||
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):
|
||||
```css
|
||||
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):
|
||||
```css
|
||||
text-shadow: 0 0 8px rgba(var-rgb, .53);
|
||||
/* ex: text-shadow: 0 0 8px #39ff7a88; */
|
||||
```
|
||||
|
||||
Glow em ícone/cor:
|
||||
```css
|
||||
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):
|
||||
|
||||
1. Cor sólida `#0a0418`
|
||||
2. Dois gradientes radiais sutis (roxo no topo, magenta no canto inferior direito)
|
||||
3. Grid pontilhado fino (32×32px linhas, opacidade 12%) sobre tudo
|
||||
4. Scanlines horizontais finas (CRT vibe) com opacidade 2,5%
|
||||
|
||||
CSS do body/root do app:
|
||||
```css
|
||||
.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.
|
||||
|
||||
```vue
|
||||
<!-- 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:
|
||||
```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`:
|
||||
|
||||
```html
|
||||
<div class="fc-panel-title">:: GASTOS · CATEGORIA</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.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":
|
||||
|
||||
```css
|
||||
.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
|
||||
|
||||
```css
|
||||
.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)
|
||||
|
||||
```vue
|
||||
<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>
|
||||
```
|
||||
|
||||
```css
|
||||
.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.
|
||||
|
||||
```html
|
||||
<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>
|
||||
```
|
||||
|
||||
```css
|
||||
.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)
|
||||
|
||||
```css
|
||||
.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)
|
||||
|
||||
```css
|
||||
@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:
|
||||
|
||||
```vue
|
||||
<!-- 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:
|
||||
|
||||
```ts
|
||||
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) ou `1fr 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
|
||||
```
|
||||
Reference in New Issue
Block a user