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:
2026-05-27 20:04:44 -03:00
co-authored by Claude Sonnet 4.6
parent 50637bd590
commit acbce4edc0
49 changed files with 2662 additions and 382 deletions
+164
View File
@@ -0,0 +1,164 @@
# Financeiro Carvalho · Handoff (Arcade Neon)
Pacote de design pronto pra ser entregue ao Claude Code / Cursor / Aider implementar.
Direção visual escolhida: **Arcade Neon**.
## O que tem aqui
```
neon-handoff/
├── STYLE_GUIDE.md ← leia primeiro
├── preview.html ← referência visual interativa (abra num servidor local)
├── tokens.css ← variáveis CSS + reset + classes utilitárias
├── components/ ← Vue 3 SFCs de referência
│ ├── NeonPanel.vue
│ ├── XPBar.vue
│ ├── HUDStat.vue
│ └── CharacterSprite.vue
├── sprites/
│ ├── sprite-data.json ← grade raw 17×24 + lista de cosméticos
│ ├── icons.json ← heart, coin, star, anchor (8×8 cada)
│ ├── manoel-default.png (e 4x)
│ ├── manoel-lv01.png (e 4x) · cabelo castanho, camisa azul, calça caqui
│ ├── manoel-lv03-bone-verolme.png · boné azul
│ ├── manoel-lv05-wingspan.png · camisa verde
│ ├── manoel-lv07-chapeu-sol.png · look "equipado" atual
│ ├── manoel-lv10-anti-vento.png · casaco roxo
│ ├── manoel-lv15-capitao.png · trajado de capitão (vermelho/preto)
│ └── manoel-sheet-4x.png · todos lado a lado pra referência
└── README.md ← este arquivo
```
## Como integrar no apps/web
1. Copie `tokens.css` para `src/styles/tokens.css`. Importe **uma vez** em `src/main.ts`:
```ts
import './styles/tokens.css'
```
2. Copie `components/*.vue` para `src/components/`. Eles já consomem `var(--fc-*)`.
3. Copie `sprites/sprite-data.json` e `sprites/icons.json` pra `src/assets/`.
Garanta que `tsconfig.json` tenha `"resolveJsonModule": true` (Vite default já tem).
4. Embrulhe o `<router-view>` em `<div class="fc-app">` — é isso que aplica o fundo + scanlines + grid.
```vue
<!-- App.vue -->
<template>
<div class="fc-app">
<AppHud />
<RouterView />
</div>
</template>
```
5. As 8 rotas do PRD (`/`, `/transacoes`, `/importar`, `/categorias`, `/recorrencias`,
`/contas`, `/personagem`, `/configuracoes`) usam o mesmo header HUD. Veja
`STYLE_GUIDE.md` §3.6 pro shape do componente.
## Sprites · uso
### Renderizar dinâmico (recomendado)
`<CharacterSprite>` lê `sprite-data.json` e aceita `theme` como prop. Cosméticos
equipados são merged no store (Pinia) e passados como `theme`.
```vue
<CharacterSprite :scale="6" :theme="gameStore.equippedTheme" :celebrate="gameStore.justLeveledUp" />
```
Exemplo de store getter:
```ts
// stores/game.ts
const equippedTheme = computed(() => {
return cosmetics.value
.filter(c => c.equipped)
.reduce((acc, c) => ({ ...acc, ...c.colors }), {} as Record<string, string>)
})
```
### Usar PNG estático
Útil pra previews em página de loja (`/personagem` → seção "cosméticos"):
```html
<img src="@/assets/sprites/manoel-lv03-bone-verolme-4x.png" alt="Boné Verolme" />
```
Sempre `image-rendering: pixelated;` no CSS pra não suavizar.
### Regenerar PNGs
`sprites/sprite-data.json` é a fonte da verdade. Se você ajustar uma cor de
cosmético lá, regenere os PNGs com um script Node:
```js
// scripts/build-sprites.mjs
import { createCanvas } from '@napi-rs/canvas' // ou node-canvas
import fs from 'node:fs/promises'
const data = JSON.parse(await fs.readFile('src/assets/sprite-data.json', 'utf8'))
// ... mesmo render do PixelSprite acima, salva PNG por cosmético
```
Ou peça pro Claude Code regenerar — o algoritmo é trivial (loop pela grade,
fillRect por célula com a cor do slot).
## Princípios para o Claude implementar
Por ordem de importância:
1. **HUD persistente em todas as rotas.** Não use sidebar — o app é um HUD de jogo.
2. **Nunca use hex direto.** Sempre `var(--fc-*)`. Se faltar token, adicione-o ao
`tokens.css` e documente no `STYLE_GUIDE.md` §1.1.
3. **Press Start 2P pra label e número. JetBrains Mono pra dado. Inter pra prosa.**
Press Start 2P sempre em maiúscula.
4. **Glow é raro.** Só nos números heroicos (47%) e no painel principal de cada
rota. Se cada card brilha, nada brilha.
5. **Verde/vermelho são binários** — meta sim / meta não. Não use vermelho pra valor
negativo (transação) — só pra alerta.
6. **Cada animação tem propósito.** Ver §6 do guia.
7. **Mobile vira bottom-nav.** Não tente espremer a HUD horizontal num iPhone.
## Telas a implementar
Conforme `STYLE_GUIDE.md` + screens visíveis em `preview.html` (Arcade · 01):
- [ ] `/` Dashboard (hero poupado, categorias, histórico, transações, recorrências, quests, patrimônio, widget personagem)
- [ ] `/transacoes` lista + CRUD
- [ ] `/importar` upload OFX/CSV + preview de deduplicação
- [ ] `/categorias` CRUD com pixel-icon picker
- [ ] `/recorrencias` lista mensal com status (paid / due / missing)
- [ ] `/contas` cards de conta + patrimônio total
- [ ] `/personagem` painel RPG completo
- [ ] `/configuracoes` formulário simples (perfil, meta %, prefs visuais)
Para qualquer tela nova: comece copiando a estrutura do dashboard,
substitua o conteúdo dos painéis, mantenha a HUD.
## Componentes adicionais que faltam
Esses não estão neste pacote — implemente seguindo os tokens já definidos:
- `AppHud.vue` (cabeçalho persistente)
- `BottomNav.vue` (mobile)
- `TransactionRow.vue`
- `QuestCard.vue`
- `CategoryBar.vue`
- `AchievementCell.vue`
- `LevelUpModal.vue` (overlay celebratório no level up)
- `MonthSwitcher.vue` ( MAIO · 2026 )
## Tom de voz
Português brasileiro, direto, com vocabulário de jogo. Não é app corporativo.
Ver `STYLE_GUIDE.md` §8.
---
Qualquer dúvida sobre **intenção visual** (não implementação), volte ao
`STYLE_GUIDE.md` ou ao `preview.html`. A página de design original do projeto
(canvas com 4 direções) também segue acessível pra comparação.
+640
View File
@@ -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 &gt;`, `REGISTRAR &gt;` — 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 7681023px: 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
```
+106
View File
@@ -0,0 +1,106 @@
<template>
<svg
:width="cols * scale"
:height="rows * scale"
:viewBox="`0 0 ${cols * scale} ${rows * scale}`"
shape-rendering="crispEdges"
class="fc-sprite"
:class="{ 'fc-sprite--bob': bob && !celebrate, 'fc-sprite--celebrate': celebrate }"
aria-label="Personagem pixel art"
>
<rect
v-for="(px, i) in pixels"
:key="i"
:x="px.x * scale"
:y="px.y * scale"
:width="scale"
:height="scale"
:fill="colors[px.k]"
/>
</svg>
</template>
<script setup lang="ts">
/**
* Renders Manoel as a 17x24 pixel sprite.
*
* The pixel grid is imported from `sprite-data.json`. Each cell's character
* is a color *slot key* — '.' is transparent, otherwise the key maps into
* `defaultColors` (which the user can override via the `theme` prop).
*
* Cosmetics work by overriding slots. For example, `Camisa Tripulante` sets
* `{ c: '#0ea5e9', C: '#0369a1' }`. Merge equipped cosmetics' color maps in
* your store and pass the result here.
*/
import { computed } from 'vue'
// In Vite/TS, ensure `resolveJsonModule: true` and adjust import path:
import spriteData from '../sprites/sprite-data.json'
const props = withDefaults(defineProps<{
/** Pixel zoom factor. 2 = mobile, 3 = widget, 4 = personagem hero (default), 6 = focus. */
scale?: number
/** Per-slot color overrides (cosmetics). Keys are sprite-data color slots. */
theme?: Partial<Record<string, string>>
/** Idle bob animation. Off when celebrate is on. */
bob?: boolean
/** Celebration pose (use after quest complete, ~3s). */
celebrate?: boolean
}>(), {
scale: 4,
bob: true,
})
const rows = spriteData.rows
const cols = spriteData.cols
const defaultColors: Record<string, string> = {
L: 'var(--fc-sprite-outline, #10131c)',
s: 'var(--fc-sprite-skin, #f3c79b)',
S: 'var(--fc-sprite-skin-shade, #c98863)',
e: 'var(--fc-sprite-eye, #10131c)',
m: 'var(--fc-sprite-mouth, #80484b)',
h: 'var(--fc-sprite-hat, #ffd84d)',
w: 'var(--fc-sprite-band, #fde68a)',
c: 'var(--fc-sprite-shirt, #00f0ff)',
C: 'var(--fc-sprite-shirt-shade, #0369a1)',
p: 'var(--fc-sprite-pants, #3a1f6e)',
P: 'var(--fc-sprite-pants-shade, #2d1857)',
b: 'var(--fc-sprite-boots, #0f172a)',
}
const colors = computed(() => ({ ...defaultColors, ...(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;
display: inline-block;
line-height: 0;
}
@keyframes fc-sprite-bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
.fc-sprite--bob { animation: fc-sprite-bob 2.2s steps(2) infinite; }
@keyframes fc-sprite-celebrate {
0%, 100% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-6px) rotate(-3deg); }
75% { transform: translateY(-6px) rotate(3deg); }
}
.fc-sprite--celebrate { animation: fc-sprite-celebrate 1s ease-in-out infinite; }
</style>
+48
View File
@@ -0,0 +1,48 @@
<template>
<div class="fc-hud-stat">
<div class="fc-hud-stat__label">{{ label }}</div>
<div class="fc-hud-stat__value">
<span :style="{ color, textShadow: `0 0 8px ${color}66` }">{{ value }}</span>
<span v-if="sub" class="fc-hud-stat__sub">{{ sub }}</span>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
/** Short uppercase label, ex: "LV", "XP", "STREAK", "META". */
label: string
/** Main value, displayed in pixel font. */
value: string | number
/** Optional fraction-style suffix, ex: "/4900" or "/40%". */
sub?: string
/** CSS color string (use var(--fc-*) tokens). Default = text color. */
color?: string
}>()
</script>
<style scoped>
.fc-hud-stat {
display: flex;
flex-direction: column;
gap: 2px;
}
.fc-hud-stat__label {
font-family: var(--fc-font-pixel);
font-size: 7px;
color: var(--fc-text-dim);
letter-spacing: .06em;
}
.fc-hud-stat__value {
display: flex;
align-items: baseline;
gap: 4px;
font-family: var(--fc-font-pixel);
font-size: 13px;
}
.fc-hud-stat__sub {
font-family: var(--fc-font-mono);
font-size: 10px;
color: var(--fc-text-dim);
}
</style>
+95
View File
@@ -0,0 +1,95 @@
<template>
<div
class="fc-panel"
:class="{ 'fc-panel--glow': glow, [`fc-panel--${variant}`]: !!variant }"
>
<template v-if="corners">
<span class="fc-corner fc-corner--tl" />
<span class="fc-corner fc-corner--tr" />
<span class="fc-corner fc-corner--bl" />
<span class="fc-corner fc-corner--br" />
</template>
<header v-if="title || $slots.header" class="fc-panel__header">
<div v-if="title" class="fc-panel__title">:: {{ title }}</div>
<slot name="header" />
</header>
<slot />
</div>
</template>
<script setup lang="ts">
defineProps<{
/** Painel "principal" da rota: borda roxa + halo. Use com parcimônia. */
glow?: boolean
/** Cantos em L decorativos. Reserve pro card de destaque. */
corners?: boolean
/** 'danger' (recorrência ausente, meta falhou) | 'success' */
variant?: 'danger' | 'success'
/** Título estilo HUD; renderizado automaticamente como `:: NOME`. */
title?: string
}>()
</script>
<style scoped>
.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: var(--fc-space-4);
box-shadow: var(--fc-shadow-panel);
}
.fc-panel--glow {
border-color: var(--fc-accent-3);
box-shadow: var(--fc-shadow-panel-glow);
}
.fc-panel--danger {
border-color: var(--fc-red);
box-shadow:
inset 0 1px 0 rgba(255,255,255,.05),
0 0 0 1px var(--fc-red),
0 0 16px rgba(255, 59, 107, .3),
0 8px 24px rgba(0,0,0,.4);
}
.fc-panel--success {
border-color: var(--fc-green);
box-shadow:
inset 0 1px 0 rgba(255,255,255,.05),
0 0 0 1px var(--fc-green),
0 0 16px rgba(57, 255, 122, .25),
0 8px 24px rgba(0,0,0,.4);
}
.fc-panel__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--fc-space-3);
gap: var(--fc-space-2);
}
.fc-panel__title {
font-family: var(--fc-font-pixel);
font-size: 9px;
color: var(--fc-accent-2);
letter-spacing: .04em;
text-transform: uppercase;
}
.fc-corner {
position: absolute;
width: 8px;
height: 8px;
border: 2px solid var(--fc-accent-2);
pointer-events: none;
}
.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; }
</style>
+57
View File
@@ -0,0 +1,57 @@
<template>
<div class="fc-bar" :class="[`fc-bar--${size}`, { 'fc-bar--success': success, 'fc-bar--danger': danger }]">
<div class="fc-bar__fill" :style="{ width: clamped + '%' }" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults(defineProps<{
/** Percentual 0100 (ou maior, será clampado). */
pct: number
/** 'sm' (6px) | 'md' (8px) | 'lg' (14px) */
size?: 'sm' | 'md' | 'lg'
/** Pinta de verde quando meta foi atingida. */
success?: boolean
/** Pinta de vermelho. */
danger?: boolean
}>(), { size: 'md' })
const clamped = computed(() => Math.max(0, Math.min(100, props.pct)))
</script>
<style scoped>
.fc-bar {
background: var(--fc-bg);
border: 2px solid var(--fc-panel-edge);
position: relative;
overflow: hidden;
}
.fc-bar--sm { height: 6px; }
.fc-bar--md { height: 8px; }
.fc-bar--lg { height: 14px; }
.fc-bar__fill {
height: 100%;
background: linear-gradient(90deg, var(--fc-accent-2), var(--fc-accent-3), var(--fc-accent));
position: relative;
transition: width .6s cubic-bezier(.2,.7,.3,1);
}
.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));
}
.fc-bar--danger { border-color: var(--fc-red); }
.fc-bar--danger .fc-bar__fill {
background: linear-gradient(90deg, var(--fc-red), var(--fc-accent));
}
</style>
@@ -0,0 +1,44 @@
Data;Estabelecimento;Portador;Valor;Parcela
;;;;
02/05/2026;PAC MOTIVA CENTRO DE E;MANOEL CARVALHO;R$ 329,00;-
02/05/2026;RECIBOM SUPERMERCADOS;MANOEL CARVALHO;R$ 369,36;-
03/01/2026;AMAZON BR;MANOEL CARVALHO;R$ 59,69;5 de 10
04/04/2026;JIM.COM SPANGHERO COMERCI;MANOEL CARVALHO;R$ 3,00;2 de 6
04/05/2026;DROGASIL 1744;MANOEL CARVALHO;R$ 59,44;-
04/05/2026;PALATO RIOMAR;MANOEL CARVALHO;R$ 16,90;-
04/05/2026;MERCADOLIVRE*MERCADOLIVRE;MANOEL CARVALHO;R$ 245,47;-
04/05/2026;RMR BARBEARIA CAFE BAR;MANOEL CARVALHO;R$ 145,00;-
04/05/2026;CLAUDE.AI SUBSCRIPTION;MANOEL CARVALHO;R$ 3,85;-
04/05/2026;CLAUDE.AI SUBSCRIPTION;MANOEL CARVALHO;R$ 110,00;-
05/04/2026;MP*FERRAMENTASME;MANOEL CARVALHO;R$ 559,84;2 de 10
06/05/2026;MP*BIADOCERIA;MANOEL CARVALHO;R$ 3,00;-
06/05/2026;IFD*IFOOD;MANOEL CARVALHO;R$ 5,95;-
07/05/2026;DROGARIA SAO PAULO SA;MANOEL CARVALHO;R$ 808,11;-
07/05/2026;KANELLE RESTAURANTE E;MANOEL CARVALHO;R$ 12,00;-
07/05/2026;TIDAL HIFI GLOBAL BR;MANOEL CARVALHO;R$ 22,80;-
08/05/2026;LOTTI RIO MAR RECIFE;MANOEL CARVALHO;R$ 244,40;-
10/05/2026;DROGASIL 1744;MANOEL CARVALHO;R$ 142,65;-
11/04/2026;A B VILELA SILVA;MANOEL CARVALHO;R$ 54,50;2 de 10
12/05/2026;MERCADOLIVRE*MERCADOLIVRE;MANOEL CARVALHO;R$ 75,77;-
13/05/2026;AMAZONMKTPLC*TOCADOTAB;MANOEL CARVALHO;R$ 58,15;1 de 6
14/05/2026;SNACK BOX;MANOEL CARVALHO;R$ 3,60;-
14/05/2026;DL*GOOGLE YOUTUB;MANOEL CARVALHO;R$ 11,90;-
15/05/2026;MIX MATEUS BOA VIAGEM;MANOEL CARVALHO;R$ 609,76;-
15/05/2026;CEMOPEL III;MANOEL CARVALHO;R$ 232,67;-
15/05/2026;CONTABILIZEI TECNOLOGIA L;MANOEL CARVALHO;R$ 255,00;-
19/05/2026;DL*GOOGLE DIREWO;MANOEL CARVALHO;R$ 25,50;-
20/05/2026;DL*GOOGLE GOOGLE;MANOEL CARVALHO;R$ 9,99;-
21/05/2026;RESTAURANTE DO SESC SH;MANOEL CARVALHO;R$ 21,74;-
22/01/2026;MP*MERCADOLIVRE;MANOEL CARVALHO;R$ 50,40;5 de 7
24/05/2026;MERCADO EXTRA 1381;MANOEL CARVALHO;R$ 278,03;-
25/04/2026;SNACK BOX;MANOEL CARVALHO;R$ -3,15;-
25/05/2026;DROGARIA SAO PAULO SA;MANOEL CARVALHO;R$ 54,54;-
26/04/2026;DL*GOOGLE PLAY P;MANOEL CARVALHO;R$ 9,90;-
27/04/2026;MERCADOLIVRE*WMCOMPONENTE;MANOEL CARVALHO;R$ 39,80;-
28/04/2026;LASA;MANOEL CARVALHO;R$ 14,49;-
28/04/2026;RESTAURANTE DO SESC SH;MANOEL CARVALHO;R$ 20,22;-
29/04/2026;KANELLE RESTAURANTE E;MANOEL CARVALHO;R$ 9,00;-
29/04/2026;CONVENIENCIA CARICE;MANOEL CARVALHO;R$ 14,00;-
29/04/2026;MP*ALIEXPRESS;MANOEL CARVALHO;R$ 244,44;-
29/04/2026;PAO E PROSA;MANOEL CARVALHO;R$ 20,00;-
30/04/2026;PALATO RIOMAR;MANOEL CARVALHO;R$ 160,90;-
1 Data Estabelecimento Portador Valor Parcela
2
3 02/05/2026 PAC MOTIVA CENTRO DE E MANOEL CARVALHO R$ 329,00 -
4 02/05/2026 RECIBOM SUPERMERCADOS MANOEL CARVALHO R$ 369,36 -
5 03/01/2026 AMAZON BR MANOEL CARVALHO R$ 59,69 5 de 10
6 04/04/2026 JIM.COM SPANGHERO COMERCI MANOEL CARVALHO R$ 3,00 2 de 6
7 04/05/2026 DROGASIL 1744 MANOEL CARVALHO R$ 59,44 -
8 04/05/2026 PALATO RIOMAR MANOEL CARVALHO R$ 16,90 -
9 04/05/2026 MERCADOLIVRE*MERCADOLIVRE MANOEL CARVALHO R$ 245,47 -
10 04/05/2026 RMR BARBEARIA CAFE BAR MANOEL CARVALHO R$ 145,00 -
11 04/05/2026 CLAUDE.AI SUBSCRIPTION MANOEL CARVALHO R$ 3,85 -
12 04/05/2026 CLAUDE.AI SUBSCRIPTION MANOEL CARVALHO R$ 110,00 -
13 05/04/2026 MP*FERRAMENTASME MANOEL CARVALHO R$ 559,84 2 de 10
14 06/05/2026 MP*BIADOCERIA MANOEL CARVALHO R$ 3,00 -
15 06/05/2026 IFD*IFOOD MANOEL CARVALHO R$ 5,95 -
16 07/05/2026 DROGARIA SAO PAULO SA MANOEL CARVALHO R$ 808,11 -
17 07/05/2026 KANELLE RESTAURANTE E MANOEL CARVALHO R$ 12,00 -
18 07/05/2026 TIDAL HIFI GLOBAL BR MANOEL CARVALHO R$ 22,80 -
19 08/05/2026 LOTTI RIO MAR RECIFE MANOEL CARVALHO R$ 244,40 -
20 10/05/2026 DROGASIL 1744 MANOEL CARVALHO R$ 142,65 -
21 11/04/2026 A B VILELA SILVA MANOEL CARVALHO R$ 54,50 2 de 10
22 12/05/2026 MERCADOLIVRE*MERCADOLIVRE MANOEL CARVALHO R$ 75,77 -
23 13/05/2026 AMAZONMKTPLC*TOCADOTAB MANOEL CARVALHO R$ 58,15 1 de 6
24 14/05/2026 SNACK BOX MANOEL CARVALHO R$ 3,60 -
25 14/05/2026 DL*GOOGLE YOUTUB MANOEL CARVALHO R$ 11,90 -
26 15/05/2026 MIX MATEUS BOA VIAGEM MANOEL CARVALHO R$ 609,76 -
27 15/05/2026 CEMOPEL III MANOEL CARVALHO R$ 232,67 -
28 15/05/2026 CONTABILIZEI TECNOLOGIA L MANOEL CARVALHO R$ 255,00 -
29 19/05/2026 DL*GOOGLE DIREWO MANOEL CARVALHO R$ 25,50 -
30 20/05/2026 DL*GOOGLE GOOGLE MANOEL CARVALHO R$ 9,99 -
31 21/05/2026 RESTAURANTE DO SESC SH MANOEL CARVALHO R$ 21,74 -
32 22/01/2026 MP*MERCADOLIVRE MANOEL CARVALHO R$ 50,40 5 de 7
33 24/05/2026 MERCADO EXTRA 1381 MANOEL CARVALHO R$ 278,03 -
34 25/04/2026 SNACK BOX MANOEL CARVALHO R$ -3,15 -
35 25/05/2026 DROGARIA SAO PAULO SA MANOEL CARVALHO R$ 54,54 -
36 26/04/2026 DL*GOOGLE PLAY P MANOEL CARVALHO R$ 9,90 -
37 27/04/2026 MERCADOLIVRE*WMCOMPONENTE MANOEL CARVALHO R$ 39,80 -
38 28/04/2026 LASA MANOEL CARVALHO R$ 14,49 -
39 28/04/2026 RESTAURANTE DO SESC SH MANOEL CARVALHO R$ 20,22 -
40 29/04/2026 KANELLE RESTAURANTE E MANOEL CARVALHO R$ 9,00 -
41 29/04/2026 CONVENIENCIA CARICE MANOEL CARVALHO R$ 14,00 -
42 29/04/2026 MP*ALIEXPRESS MANOEL CARVALHO R$ 244,44 -
43 29/04/2026 PAO E PROSA MANOEL CARVALHO R$ 20,00 -
44 30/04/2026 PALATO RIOMAR MANOEL CARVALHO R$ 160,90 -
+454
View File
@@ -0,0 +1,454 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<title>Financeiro Carvalho · Style Guide · Arcade Neon</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="stylesheet" href="tokens.css" />
<style>
/* preview-page-only chrome */
body { padding: 40px 32px 80px; max-width: 1200px; margin: 0 auto; }
h1 { font-family: var(--fc-font-pixel); font-size: 18px; color: var(--fc-accent-2); margin: 0 0 8px; }
h2 { font-family: var(--fc-font-pixel); font-size: 13px; color: var(--fc-accent); margin: 56px 0 6px; padding-bottom: 14px; border-bottom: 1px dashed var(--fc-panel-edge); letter-spacing: .04em; }
h2::before { content: ":: "; color: var(--fc-text-dim); }
p.lede { font-family: var(--fc-font-body); color: var(--fc-text-dim); margin: 0 0 32px; max-width: 720px; line-height: 1.6; }
h3 { font-family: var(--fc-font-pixel); font-size: 10px; color: var(--fc-text); letter-spacing: .04em; margin: 28px 0 12px; }
.grid { display: grid; gap: 16px; }
.g2 { grid-template-columns: 1fr 1fr; }
.g3 { grid-template-columns: 1fr 1fr 1fr; }
.g4 { grid-template-columns: repeat(4, 1fr); }
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
/* swatch card */
.swatch { background: linear-gradient(180deg, var(--fc-bg-raised), var(--fc-bg-panel));
border: 2px solid var(--fc-panel-edge); border-radius: 4px;
padding: 0; overflow: hidden; box-shadow: var(--fc-shadow-panel); }
.swatch__chip { height: 80px; }
.swatch__body { padding: 10px 12px; }
.swatch__token { font-family: var(--fc-font-mono); font-size: 11px; color: var(--fc-text); font-weight: 600; }
.swatch__hex { font-family: var(--fc-font-mono); font-size: 10px; color: var(--fc-text-dim); margin-top: 3px; }
.swatch__note { font-family: var(--fc-font-body); font-size: 11px; color: var(--fc-text-dim); margin-top: 6px; line-height: 1.4; }
/* tokens / labels */
.label { font-family: var(--fc-font-pixel); font-size: 7px; color: var(--fc-text-dim); letter-spacing: .06em; margin-bottom: 6px; }
/* generic demo panel */
.demo {
background: linear-gradient(180deg, var(--fc-bg-raised), var(--fc-bg-panel));
border: 2px solid var(--fc-panel-edge);
border-radius: 4px;
padding: 16px;
box-shadow: var(--fc-shadow-panel);
position: relative;
}
.demo--glow { border-color: var(--fc-accent-3); box-shadow: var(--fc-shadow-panel-glow); }
.demo--danger { border-color: var(--fc-red); box-shadow: 0 0 0 1px var(--fc-red), 0 0 16px rgba(255,59,107,.3); }
.corner { position: absolute; width: 8px; height: 8px; border: 2px solid var(--fc-accent-2); }
.corner.tl { top:-2px; left:-2px; border-right:none; border-bottom:none; }
.corner.tr { top:-2px; right:-2px; border-left:none; border-bottom:none; }
.corner.bl { bottom:-2px; left:-2px; border-right:none; border-top:none; }
.corner.br { bottom:-2px; right:-2px; border-left:none; border-top:none; }
.ptitle { font-family: var(--fc-font-pixel); font-size: 9px; color: var(--fc-accent-2); margin-bottom: 12px; letter-spacing: .04em; }
/* buttons */
.btn {
font-family: var(--fc-font-pixel); 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;
}
.btn:hover { background: var(--fc-accent-3); color: white; box-shadow: 0 0 16px rgba(168,85,247,.6); }
.btn--pink { border-color: var(--fc-accent); }
.btn--pink:hover { background: var(--fc-accent); box-shadow: 0 0 16px rgba(255,45,149,.6); }
.btn--cyan { border-color: var(--fc-accent-2); color: var(--fc-accent-2); }
.btn--cyan:hover { background: var(--fc-accent-2); color: var(--fc-bg); box-shadow: 0 0 16px rgba(0,240,255,.6); }
/* chips */
.chip { display: inline-block; font-family: var(--fc-font-pixel); font-size: 7px;
padding: 4px 6px; border-radius: 2px; letter-spacing: .05em; color: #000; }
.chip--magenta { background: var(--fc-accent); }
.chip--cyan { background: var(--fc-accent-2); }
.chip--gold { background: var(--fc-gold); }
.chip--green { background: var(--fc-green); }
.chip--red { background: var(--fc-red); color: #fff; }
/* bar */
.bar { height: 14px; background: var(--fc-bg); border: 2px solid var(--fc-panel-edge); position: relative; overflow: hidden; }
.bar__fill { height: 100%;
background: linear-gradient(90deg, var(--fc-accent-2), var(--fc-accent-3), var(--fc-accent));
position: relative; }
.bar__fill::after { content:''; position:absolute; inset:0;
background-image: repeating-linear-gradient(90deg, rgba(0,0,0,.2) 0 2px, transparent 2px 6px); }
.bar--success { border-color: var(--fc-green); }
.bar--success .bar__fill { background: linear-gradient(90deg, var(--fc-green), var(--fc-accent-2)); }
/* link */
.link { color: var(--fc-accent-2); font-family: var(--fc-font-pixel); font-size: 8px;
cursor: pointer; text-decoration: none; letter-spacing: .04em; }
.link:hover { text-decoration: underline; text-shadow: 0 0 8px var(--fc-accent-2); }
/* font sample */
.font-sample { display: grid; grid-template-columns: 140px 1fr; align-items: baseline; gap: 16px; padding: 12px 0; border-bottom: 1px dashed var(--fc-panel-edge); }
.font-sample__meta { font-family: var(--fc-font-mono); font-size: 11px; color: var(--fc-text-dim); }
.font-sample__demo .pix { font-family: var(--fc-font-pixel); }
.font-sample__demo .mon { font-family: var(--fc-font-mono); }
/* sprite cards */
.sprite-card { background: linear-gradient(180deg, var(--fc-bg-raised), var(--fc-bg-panel));
border: 2px solid var(--fc-panel-edge); border-radius: 4px;
padding: 16px 12px 12px; text-align: center;
box-shadow: var(--fc-shadow-panel); }
.sprite-card svg, .sprite-card img { display: block; margin: 0 auto 10px; image-rendering: pixelated; }
.sprite-card__name { font-family: var(--fc-font-pixel); font-size: 8px; color: var(--fc-text); margin-bottom: 4px; }
.sprite-card__meta { font-family: var(--fc-font-pixel); font-size: 6px; color: var(--fc-gold); letter-spacing: .06em; }
/* example HUD */
.hud-demo {
display: flex; align-items: center; gap: 16px; padding: 12px 20px;
border: 2px solid var(--fc-panel-edge);
background: linear-gradient(180deg, var(--fc-bg-panel), var(--fc-bg));
border-radius: 4px;
box-shadow: var(--fc-shadow-panel);
}
.hud-demo__logo { display: flex; align-items: center; gap: 10px; }
.hud-demo__mark { width: 28px; height: 28px; background: var(--fc-accent); box-shadow: 0 0 12px var(--fc-accent); position: relative; }
.hud-demo__mark::before { content:''; position:absolute; inset:5px; background: var(--fc-bg); }
.hud-demo__mark::after { content:''; position:absolute; top:9px; left:9px; width:10px; height:10px; background: var(--fc-accent-2); }
.stat { display: flex; flex-direction: column; gap: 2px; }
.stat__l { font-family: var(--fc-font-pixel); font-size: 7px; color: var(--fc-text-dim); }
.stat__v { display: flex; align-items: baseline; gap: 4px; font-family: var(--fc-font-pixel); font-size: 13px; }
.stat__s { font-family: var(--fc-font-mono); font-size: 10px; color: var(--fc-text-dim); }
code { font-family: var(--fc-font-mono); font-size: 11px; color: var(--fc-accent-2); background: rgba(0,240,255,.08); padding: 1px 6px; border-radius: 3px; }
hr { border: 0; border-top: 1px dashed var(--fc-panel-edge); margin: 32px 0; }
</style>
</head>
<body class="fc-app">
<header>
<div class="hud-demo">
<div class="hud-demo__logo">
<div class="hud-demo__mark"></div>
<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);margin-top:2px">STYLE GUIDE · ARCADE NEON</div>
</div>
</div>
<div style="flex:1"></div>
<div class="stat">
<span class="stat__l">VER</span>
<span class="stat__v" style="color:var(--fc-gold);text-shadow:0 0 8px rgba(255,216,77,.4)">1.0</span>
</div>
<div class="stat">
<span class="stat__l">DATA</span>
<span class="stat__v" style="color:var(--fc-accent-2);font-size:11px">26/05/26</span>
</div>
</div>
</header>
<h1 style="margin-top:32px">Style Guide · Financeiro Carvalho</h1>
<p class="lede">Referência visual da direção <strong>Arcade Neon</strong>. Use junto do <code>STYLE_GUIDE.md</code>. Tokens vivem em <code>tokens.css</code> — esta página apenas os exibe. Componentes Vue prontos pra copiar estão em <code>components/</code>.</p>
<!-- ─────────────── COLORS ─────────────── -->
<h2>Colors</h2>
<h3>Estrutura (escuro)</h3>
<div class="grid g4">
<div class="swatch"><div class="swatch__chip" style="background:#0a0418"></div><div class="swatch__body"><div class="swatch__token">--fc-bg</div><div class="swatch__hex">#0a0418</div><div class="swatch__note">Fundo base do app</div></div></div>
<div class="swatch"><div class="swatch__chip" style="background:#150827"></div><div class="swatch__body"><div class="swatch__token">--fc-bg-raised</div><div class="swatch__hex">#150827</div><div class="swatch__note">Topo do gradiente do painel</div></div></div>
<div class="swatch"><div class="swatch__chip" style="background:#1c0d33"></div><div class="swatch__body"><div class="swatch__token">--fc-bg-panel</div><div class="swatch__hex">#1c0d33</div><div class="swatch__note">Base do gradiente do painel</div></div></div>
<div class="swatch"><div class="swatch__chip" style="background:#2d1857"></div><div class="swatch__body"><div class="swatch__token">--fc-panel-edge</div><div class="swatch__hex">#2d1857</div><div class="swatch__note">Borda padrão de painel + dividers</div></div></div>
</div>
<h3>Texto</h3>
<div class="grid g2">
<div class="swatch"><div class="swatch__chip" style="background:#f4eaff"></div><div class="swatch__body"><div class="swatch__token">--fc-text</div><div class="swatch__hex">#f4eaff</div><div class="swatch__note">Texto principal. Off-white com tom lilás.</div></div></div>
<div class="swatch"><div class="swatch__chip" style="background:#8b75b8"></div><div class="swatch__body"><div class="swatch__token">--fc-text-dim</div><div class="swatch__hex">#8b75b8</div><div class="swatch__note">Texto secundário, labels, sub.</div></div></div>
</div>
<h3>Acentos</h3>
<div class="grid g4">
<div class="swatch"><div class="swatch__chip" style="background:#ff2d95;box-shadow:inset 0 0 24px rgba(0,0,0,.3)"></div><div class="swatch__body"><div class="swatch__token">--fc-accent</div><div class="swatch__hex">#ff2d95</div><div class="swatch__note">Magenta · alertas suaves, quest diária, CTA primário accent.</div></div></div>
<div class="swatch"><div class="swatch__chip" style="background:#00f0ff"></div><div class="swatch__body"><div class="swatch__token">--fc-accent-2</div><div class="swatch__hex">#00f0ff</div><div class="swatch__note">Ciano · links, navegação ativa, XP, quest semanal.</div></div></div>
<div class="swatch"><div class="swatch__chip" style="background:#a855f7"></div><div class="swatch__body"><div class="swatch__token">--fc-accent-3</div><div class="swatch__hex">#a855f7</div><div class="swatch__note">Roxo · botão padrão, painel em destaque, glow.</div></div></div>
<div class="swatch"><div class="swatch__chip" style="background:#ffd84d"></div><div class="swatch__body"><div class="swatch__token">--fc-gold</div><div class="swatch__hex">#ffd84d</div><div class="swatch__note">XP, conquistas, quest mensal.</div></div></div>
</div>
<h3>Estado</h3>
<div class="grid g2">
<div class="swatch"><div class="swatch__chip" style="background:#39ff7a"></div><div class="swatch__body"><div class="swatch__token">--fc-green</div><div class="swatch__hex">#39ff7a</div><div class="swatch__note">Meta atingida, valores positivos.</div></div></div>
<div class="swatch"><div class="swatch__chip" style="background:#ff3b6b"></div><div class="swatch__body"><div class="swatch__token">--fc-red</div><div class="swatch__hex">#ff3b6b</div><div class="swatch__note">Meta falhou, recorrência ausente, alerta crítico.</div></div></div>
</div>
<!-- ─────────────── TYPOGRAPHY ─────────────── -->
<h2>Typography</h2>
<div class="demo" style="padding: 0 20px">
<div class="font-sample">
<div class="font-sample__meta">Press Start 2P<br><span style="color:var(--fc-text-dim);font-size:9px">pixel · labels & numbers</span></div>
<div class="font-sample__demo">
<div class="pix" style="font-size:7px;color:var(--fc-text-dim)">:: LABEL HUD ::</div>
<div class="pix" style="font-size:9px;color:var(--fc-accent-2);margin-top:6px">:: TÍTULO DE PAINEL</div>
<div class="pix" style="font-size:14px;color:var(--fc-text);margin-top:6px">MAIO · 2026</div>
<div class="pix" style="font-size:40px;color:var(--fc-green);text-shadow:0 0 12px #39ff7a88;margin-top:8px">47%</div>
</div>
</div>
<div class="font-sample">
<div class="font-sample__meta">JetBrains Mono<br><span style="color:var(--fc-text-dim);font-size:9px">mono · valores</span></div>
<div class="font-sample__demo">
<div class="mon" style="font-size:10px;color:var(--fc-text-dim)">26/05/26 · Itaú · Crédito</div>
<div class="mon" style="font-size:11px;color:var(--fc-text);margin-top:4px">Mercado Pão de Açúcar</div>
<div class="mon" style="font-size:16px;color:var(--fc-text);font-weight:600;margin-top:4px">R$ 7.844,21</div>
</div>
</div>
<div class="font-sample" style="border-bottom:none">
<div class="font-sample__meta">Inter<br><span style="color:var(--fc-text-dim);font-size:9px">body · descrições</span></div>
<div class="font-sample__demo">
<div class="fc-body" style="font-size:13px;color:var(--fc-text);line-height:1.55;max-width:520px">
Mantenha gastos com veleiro abaixo de R$ 500 neste mês. Recompensa: <span class="fc-text-gold">+180 XP</span>. Última atualização há 2h.
</div>
</div>
</div>
</div>
<!-- ─────────────── PANELS ─────────────── -->
<h2>Panels</h2>
<p class="lede">Painel é o tijolo do app. Versão glow é reservada pro card de destaque da rota (poupado-do-mês, status do personagem). Cantos em "L" decoram só painéis especiais.</p>
<div class="grid g2">
<div class="demo">
<div class="ptitle">:: PAINEL PADRÃO</div>
<p class="fc-body" style="margin:0;color:var(--fc-text);font-size:13px;line-height:1.5">Container neutro. Use pra listar transações, categorias, contas, qualquer dado tabular.</p>
</div>
<div class="demo demo--glow">
<div class="ptitle">:: PAINEL COM GLOW</div>
<p class="fc-body" style="margin:0;color:var(--fc-text);font-size:13px;line-height:1.5">Borda roxa + halo. Reservado pro card mais importante da rota.</p>
</div>
<div class="demo">
<span class="corner tl"></span><span class="corner tr"></span><span class="corner bl"></span><span class="corner br"></span>
<div class="ptitle">:: PAINEL COM CANTOS EM L</div>
<p class="fc-body" style="margin:0;color:var(--fc-text);font-size:13px;line-height:1.5">Decoração HUD opcional. Use no hero ou no card-de-status do personagem.</p>
</div>
<div class="demo demo--danger">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
<span class="fc-blink" style="color:var(--fc-red);font-size:16px"></span>
<span class="fc-pixel" style="font-size:8px;color:var(--fc-red)">RECORRÊNCIA AUSENTE</span>
</div>
<p class="fc-body" style="margin:0;color:var(--fc-text);font-size:13px">Variante <code>danger</code>. Borda vermelha + halo + triângulo pulsante.</p>
</div>
</div>
<!-- ─────────────── BUTTONS ─────────────── -->
<h2>Buttons</h2>
<p class="lede">Texto sempre Press Start 2P em maiúscula. Verbo + (seta opcional). Hover acende o glow da cor da borda.</p>
<div class="row">
<button class="btn">REGISTRAR ▶</button>
<button class="btn btn--pink">IMPORTAR EXTRATO</button>
<button class="btn btn--cyan">VER DETALHES</button>
</div>
<!-- ─────────────── CHIPS ─────────────── -->
<h2>Chips</h2>
<p class="lede">Tags pequenas. Sempre Press Start 2P 7px maiúsculo. A cor é semântica.</p>
<div class="row">
<span class="chip chip--magenta">DIÁRIA</span>
<span class="chip chip--cyan">SEMANAL</span>
<span class="chip chip--gold">MENSAL</span>
<span class="chip chip--green">QUEST OK</span>
<span class="chip chip--red">QUEST FAIL</span>
</div>
<!-- ─────────────── BARS ─────────────── -->
<h2>Bars (XP, progresso)</h2>
<p class="lede">Sempre 2px de borda. Fill com gradiente diagonal + listras internas — assinatura visual.</p>
<div class="demo">
<div style="display:flex;justify-content:space-between;margin-bottom:6px">
<span class="fc-pixel" style="font-size:7px;color:var(--fc-text-dim)">XP</span>
<span class="fc-mono" style="font-size:10px;color:var(--fc-text-dim)">3240 / 4900</span>
</div>
<div class="bar" style="height:14px">
<div class="bar__fill" style="width:66%"></div>
</div>
<div style="display:flex;justify-content:space-between;margin:14px 0 6px">
<span class="fc-pixel" style="font-size:7px;color:var(--fc-text-dim)">PROGRESSO QUEST · DIÁRIA</span>
<span class="fc-mono" style="font-size:10px;color:var(--fc-text-dim)">3 / 5</span>
</div>
<div class="bar" style="height:6px"><div class="bar__fill" style="width:60%"></div></div>
<div style="display:flex;justify-content:space-between;margin:14px 0 6px">
<span class="fc-pixel" style="font-size:7px;color:var(--fc-text-dim)">META POUPANÇA · ATINGIDA</span>
<span class="fc-mono" style="font-size:10px;color:var(--fc-green)">47%</span>
</div>
<div class="bar bar--success" style="height:14px"><div class="bar__fill" style="width:47%"></div></div>
</div>
<!-- ─────────────── LINKS ─────────────── -->
<h2>Links (text actions)</h2>
<p class="lede">Verbo curto + seta. Sempre Press Start 2P em ciano. Hover ganha glow.</p>
<div class="row">
<a class="link">VER+</a>
<a class="link">ABRIR ▶</a>
<a class="link">REGISTRAR ▶</a>
</div>
<!-- ─────────────── HUD ─────────────── -->
<h2>HUD (top bar)</h2>
<p class="lede">Persistente em todas as rotas. Os stats RPG (LV/XP/STREAK/META) sempre visíveis — é assim que a direção mantém a metáfora de jogo.</p>
<div class="hud-demo">
<div class="hud-demo__logo">
<div class="hud-demo__mark"></div>
<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);margin-top:2px">FIN.SYS v1.4</div>
</div>
</div>
<div style="width:2px;height:32px;background:var(--fc-panel-edge);margin-inline:8px"></div>
<div class="stat">
<span class="stat__l">LV</span>
<span class="stat__v"><span style="color:var(--fc-gold);text-shadow:0 0 8px #ffd84d66">7</span></span>
</div>
<div class="stat">
<span class="stat__l">XP</span>
<span class="stat__v"><span style="color:var(--fc-accent-2);text-shadow:0 0 8px #00f0ff66">3240</span><span class="stat__s">/4900</span></span>
</div>
<div class="stat">
<span class="stat__l">STREAK</span>
<span class="stat__v"><span style="color:var(--fc-accent);text-shadow:0 0 8px #ff2d9566">23d</span></span>
</div>
<div class="stat">
<span class="stat__l">META</span>
<span class="stat__v"><span style="color:var(--fc-green);text-shadow:0 0 8px #39ff7a66">47%</span><span class="stat__s">/40%</span></span>
</div>
</div>
<!-- ─────────────── SPRITES ─────────────── -->
<h2>Pixel Sprites</h2>
<p class="lede">Personagem é 17×24 pixels. Cores parametrizadas por slot (hat / shirt / pants / boots). Cosméticos do banco fazem merge desses slots em runtime.</p>
<h3>Escalas canônicas</h3>
<div class="grid g4" id="scales"></div>
<h3>Variantes por cosmético</h3>
<div class="grid g4" id="variants"></div>
<!-- ─────────────── ANIMATIONS ─────────────── -->
<h2>Animations</h2>
<p class="lede">Toda animação tem propósito. Idle bob é o único motion "passivo". Celebrate roda 3s após quest complete. Level-up sobrepõe modal com glow magenta.</p>
<div class="grid g3">
<div class="demo" style="text-align:center">
<div class="ptitle">IDLE BOB</div>
<div id="bob-demo" style="line-height:0;margin:14px 0 8px"></div>
<span class="fc-mono" style="font-size:10px;color:var(--fc-text-dim)">animation: 2.2s steps(2)</span>
</div>
<div class="demo" style="text-align:center">
<div class="ptitle">CELEBRATE</div>
<div id="celebrate-demo" style="line-height:0;margin:14px 0 8px"></div>
<span class="fc-mono" style="font-size:10px;color:var(--fc-text-dim)">animation: 1s ease-in-out</span>
</div>
<div class="demo" style="text-align:center">
<div class="ptitle">ALERT BLINK</div>
<div style="font-size:48px;line-height:1;margin:14px 0 8px"><span class="fc-blink" style="color:var(--fc-red);text-shadow:0 0 14px #ff3b6b"></span></div>
<span class="fc-mono" style="font-size:10px;color:var(--fc-text-dim)">animation: 1.2s steps(1)</span>
</div>
</div>
<!-- ─────────────── SHADOWS ─────────────── -->
<h2>Shadows & Glow</h2>
<div class="grid g3">
<div style="background:var(--fc-bg-raised);border:2px solid var(--fc-panel-edge);padding:24px;border-radius:4px;text-align:center;
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)">
<div class="fc-pixel" style="font-size:8px;color:var(--fc-accent-2)">PADRÃO</div>
<div class="fc-mono" style="font-size:9px;color:var(--fc-text-dim);margin-top:6px">--fc-shadow-panel</div>
</div>
<div style="background:var(--fc-bg-raised);border:2px solid var(--fc-accent-3);padding:24px;border-radius:4px;text-align:center;
box-shadow:var(--fc-shadow-panel-glow)">
<div class="fc-pixel" style="font-size:8px;color:var(--fc-accent-3)">GLOW</div>
<div class="fc-mono" style="font-size:9px;color:var(--fc-text-dim);margin-top:6px">--fc-shadow-panel-glow</div>
</div>
<div style="background:var(--fc-bg-raised);border:2px solid var(--fc-red);padding:24px;border-radius:4px;text-align:center;
box-shadow:0 0 0 1px var(--fc-red), 0 0 16px rgba(255,59,107,.3), 0 8px 24px rgba(0,0,0,.4)">
<div class="fc-pixel" style="font-size:8px;color:var(--fc-red)">DANGER</div>
<div class="fc-mono" style="font-size:9px;color:var(--fc-text-dim);margin-top:6px">--fc-red border + halo</div>
</div>
</div>
<hr>
<p class="lede" style="margin-top:24px;font-size:11px">
<span class="fc-pixel" style="font-size:8px;color:var(--fc-text-dim)">v1.0 · </span>
Style guide gerado automaticamente. Para regenerar sprites: rode <code>run.js</code> em <code>sprites/</code>. Para perguntas, abra <code>STYLE_GUIDE.md</code> primeiro.
</p>
<script>
// Render sprite scales + variants from sprite-data.json
fetch('sprites/sprite-data.json').then(r => r.json()).then(data => {
const GRID = data.grid;
const COLS = data.cols;
const ROWS = data.rows;
const DEFAULTS = data._legend;
function spriteSvg(theme, scale, className) {
const w = COLS * scale, h = ROWS * scale;
let rects = '';
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
const k = GRID[y][x];
if (k === '.' || !k) continue;
const color = (theme && theme[k]) || DEFAULTS[k];
if (!color) continue;
rects += `<rect x="${x*scale}" y="${y*scale}" width="${scale}" height="${scale}" fill="${color}"/>`;
}
}
return `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" shape-rendering="crispEdges" class="${className||''}" style="image-rendering:pixelated">${rects}</svg>`;
}
// Scales
const defaultTheme = data.cosmetics.find(c => c.id === 'default').theme;
document.getElementById('scales').innerHTML = [
{ scale: 2, label: 'scale=2', sub: 'mobile inline' },
{ scale: 3, label: 'scale=3', sub: 'dashboard widget' },
{ scale: 4, label: 'scale=4', sub: 'padrão' },
{ scale: 6, label: 'scale=6', sub: 'personagem hero' },
].map(({scale, label, sub}) => `
<div class="sprite-card">
${spriteSvg(defaultTheme, scale)}
<div class="sprite-card__name">${label.toUpperCase()}</div>
<div class="sprite-card__meta">${sub.toUpperCase()}</div>
</div>
`).join('');
// Variants
document.getElementById('variants').innerHTML = data.cosmetics.map(cos => `
<div class="sprite-card">
${spriteSvg(cos.theme || {}, 4)}
<div class="sprite-card__name">${cos.name.toUpperCase()}</div>
<div class="sprite-card__meta">LV ${cos.unlockLv}</div>
</div>
`).join('');
// Bob demo
document.getElementById('bob-demo').innerHTML = spriteSvg(defaultTheme, 3, 'demo-bob');
document.getElementById('celebrate-demo').innerHTML = spriteSvg(defaultTheme, 3, 'demo-celebrate');
// animations
const s = document.createElement('style');
s.textContent = `
@keyframes b-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
.demo-bob { animation: b-bob 2.2s steps(2) infinite; }
@keyframes b-cel { 0%,100% { transform: translateY(0) rotate(0); } 25% { transform: translateY(-6px) rotate(-3deg); } 75% { transform: translateY(-6px) rotate(3deg); } }
.demo-celebrate { animation: b-cel 1s ease-in-out infinite; }
`;
document.head.appendChild(s);
}).catch(e => {
document.getElementById('scales').innerHTML = '<p style="color:var(--fc-red)">Erro carregando sprites: ' + e.message + '. Abra este arquivo via servidor HTTP, não com file://.</p>';
});
</script>
</body>
</html>
+67
View File
@@ -0,0 +1,67 @@
{
"_note": "Each icon is a flat grid of single-char keys. '.' = transparent. Colors are filled at render time from CSS tokens or props.",
"heart": {
"rows": 8,
"cols": 8,
"grid": [
"........",
".XX..XX.",
"XXXXXXXX",
"XXXXXXXX",
".XXXXXX.",
"..XXXX..",
"...XX...",
"........"
],
"defaultColor": "var(--fc-red, #ff3b6b)"
},
"coin": {
"rows": 8,
"cols": 8,
"grid": [
"..XXXX..",
".XYYYYX.",
"XYYSYYYX",
"XYYSYYYX",
"XYYSYYYX",
"XYYSYYYX",
".XYYYYX.",
"..XXXX.."
],
"colors": {
"X": "var(--fc-gold, #ffd84d)",
"Y": "var(--fc-gold, #ffd84d)",
"S": "#a16207"
}
},
"star": {
"rows": 8,
"cols": 8,
"grid": [
"...XX...",
"...XX...",
"XX.XX.XX",
".XXXXXX.",
"..XXXX..",
".XXXXXX.",
"XX.XX.XX",
"...XX..."
],
"defaultColor": "var(--fc-gold, #ffd84d)"
},
"anchor": {
"rows": 8,
"cols": 8,
"grid": [
"...XX...",
"..XXXX..",
"...XX...",
"XXXXXXXX",
"...XX...",
"X..XX..X",
"XXXXXXXX",
"..XXXX.."
],
"defaultColor": "var(--fc-accent-2, #00f0ff)"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

+160
View File
@@ -0,0 +1,160 @@
{
"name": "manoel",
"rows": 24,
"cols": 17,
"_colorSlots": {
"L": "outline (immutable)",
"s": "skin highlight (immutable)",
"S": "skin shade (immutable)",
"e": "eye (immutable)",
"m": "mouth (immutable)",
"h": "hat main",
"w": "hat band",
"c": "shirt main",
"C": "shirt shade",
"p": "pants main",
"P": "pants shade",
"b": "boots"
},
"_legend": {
".": "transparent",
"L": "#10131c",
"s": "#f3c79b",
"S": "#c98863",
"e": "#10131c",
"m": "#80484b",
"h": "#ffd84d",
"w": "#fde68a",
"c": "#00f0ff",
"C": "#0369a1",
"p": "#3a1f6e",
"P": "#2d1857",
"b": "#0f172a"
},
"grid": [
"....LLLLLLLLL....",
"..LLhhhhhhhhhLL..",
".LhhhhhhhhhhhhhL.",
"LLhhhhhhhhhhhhhLL",
"LwwwwwwwwwwwwwwwL",
"LLLLLLLLLLLLLLLLL",
"....LssssssssL...",
"...LssssssssssL..",
"..LsseSssssSesL..",
"..LssssssssssL...",
"..LSsLmmmmmLSsL..",
"...LSssssssSSL...",
"....LLsssssLL....",
"...LccccccccL....",
"..LcccCwwCccccL..",
".LccccCwwCccccL..",
".LccccCwwCccccL..",
".LCCCCCCCCCCCCL..",
"..LpppLLLLpppL...",
"..LppppppppppL...",
"..LppppppppppL...",
"..LPPPPPPPPPPL...",
"..LLbbLL.LLbbLL..",
"...LLLL...LLLL..."
],
"cosmetics": [
{
"id": "default",
"name": "Look Equipado (atual)",
"unlockLv": 7,
"preview": "manoel-default",
"theme": {}
},
{
"id": "lv01-tripulante",
"name": "Tripulante Iniciante",
"unlockLv": 1,
"preview": "manoel-lv01",
"_notes": "sem chapéu — h/w viram cabelo castanho",
"theme": {
"h": "#5a3a1d",
"w": "#5a3a1d",
"c": "#0ea5e9",
"C": "#075985",
"p": "#a16207",
"P": "#7c4a08",
"b": "#1e293b"
}
},
{
"id": "lv03-bone-verolme",
"name": "Boné Verolme",
"unlockLv": 3,
"preview": "manoel-lv03-bone-verolme",
"theme": {
"h": "#1e40af",
"w": "#fde68a",
"c": "#0ea5e9",
"C": "#075985",
"p": "#a16207",
"P": "#7c4a08",
"b": "#1e293b"
}
},
{
"id": "lv05-wingspan",
"name": "Camiseta Wingspan",
"unlockLv": 5,
"preview": "manoel-lv05-wingspan",
"theme": {
"h": "#ffd84d",
"w": "#fde68a",
"c": "#10b981",
"C": "#065f46",
"p": "#3a1f6e",
"P": "#2d1857",
"b": "#0f172a"
}
},
{
"id": "lv07-chapeu-sol",
"name": "Chapéu de Sol",
"unlockLv": 7,
"preview": "manoel-lv07-chapeu-sol",
"theme": {
"h": "#ffd84d",
"w": "#fde68a",
"c": "#00f0ff",
"C": "#0369a1",
"p": "#3a1f6e",
"P": "#2d1857",
"b": "#0f172a"
}
},
{
"id": "lv10-anti-vento",
"name": "Casaco Anti-Vento",
"unlockLv": 10,
"preview": "manoel-lv10-anti-vento",
"theme": {
"h": "#1e40af",
"w": "#fde68a",
"c": "#a855f7",
"C": "#581c87",
"p": "#1e293b",
"P": "#0f172a",
"b": "#0f172a"
}
},
{
"id": "lv15-capitao",
"name": "Trajado de Capitão",
"unlockLv": 15,
"preview": "manoel-lv15-capitao",
"theme": {
"h": "#ffd84d",
"w": "#dc2626",
"c": "#dc2626",
"C": "#7a0e0e",
"p": "#1c1917",
"P": "#0c0a09",
"b": "#0c0a09"
}
}
]
}
+140
View File
@@ -0,0 +1,140 @@
/* Financeiro Carvalho · Arcade Neon
* Drop this into src/styles/tokens.css and import once in main.ts:
* import './styles/tokens.css'
*
* All other components MUST consume var(--fc-*) instead of literal hex.
*/
@import url('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');
:root {
/* ─────────────── color tokens ─────────────── */
--fc-bg: #0a0418;
--fc-bg-raised: #150827;
--fc-bg-panel: #1c0d33;
--fc-panel-edge: #2d1857;
--fc-line: #3a1f6e;
--fc-text: #f4eaff;
--fc-text-dim: #8b75b8;
--fc-accent: #ff2d95; /* magenta — alerts, "today", primary CTA accent */
--fc-accent-2: #00f0ff; /* cyan — links, nav, "weekly" */
--fc-accent-3: #a855f7; /* purple — structure, glow, default button */
--fc-gold: #ffd84d; /* XP, achievements, "monthly" */
--fc-green: #39ff7a; /* goal hit, positive value */
--fc-red: #ff3b6b; /* goal missed, critical alert */
/* rgb fragments for shadows / alphas (no color-mix support? use these) */
--fc-accent-rgb: 255 45 149;
--fc-accent-2-rgb: 0 240 255;
--fc-accent-3-rgb: 168 85 247;
--fc-gold-rgb: 255 216 77;
--fc-green-rgb: 57 255 122;
--fc-red-rgb: 255 59 107;
/* ─────────────── sprite color slots ─────────────── */
--fc-sprite-outline: #10131c;
--fc-sprite-skin: #f3c79b;
--fc-sprite-skin-shade: #c98863;
--fc-sprite-eye: #10131c;
--fc-sprite-mouth: #80484b;
--fc-sprite-hat: #ffd84d;
--fc-sprite-band: #fde68a;
--fc-sprite-shirt: #00f0ff;
--fc-sprite-shirt-shade: #0369a1;
--fc-sprite-pants: #3a1f6e;
--fc-sprite-pants-shade: #2d1857;
--fc-sprite-boots: #0f172a;
/* ─────────────── sizing ─────────────── */
--fc-radius-sm: 2px;
--fc-radius: 4px;
--fc-radius-lg: 6px;
--fc-space-1: 4px;
--fc-space-2: 8px;
--fc-space-3: 12px;
--fc-space-4: 16px;
--fc-space-5: 24px;
/* ─────────────── shadows ─────────────── */
--fc-shadow-panel:
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-shadow-panel-glow:
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);
/* ─────────────── fonts ─────────────── */
--fc-font-pixel: 'Press Start 2P', monospace;
--fc-font-mono: 'JetBrains Mono', ui-monospace, monospace;
--fc-font-body: 'Inter', system-ui, sans-serif;
}
/* Global reset for the app root */
html, body {
margin: 0;
padding: 0;
background: var(--fc-bg);
color: var(--fc-text);
font-family: var(--fc-font-body);
}
* { box-sizing: border-box; }
/* Util classes */
.fc-pixel { font-family: var(--fc-font-pixel); letter-spacing: .02em; }
.fc-mono { font-family: var(--fc-font-mono); }
.fc-body { font-family: var(--fc-font-body); }
.fc-text-dim { color: var(--fc-text-dim); }
.fc-text-accent { color: var(--fc-accent); }
.fc-text-accent2 { color: var(--fc-accent-2); }
.fc-text-accent3 { color: var(--fc-accent-3); }
.fc-text-gold { color: var(--fc-gold); }
.fc-text-green { color: var(--fc-green); }
.fc-text-red { color: var(--fc-red); }
.fc-glow-cyan { text-shadow: 0 0 8px rgba(0,240,255,.6); }
.fc-glow-magenta { text-shadow: 0 0 8px rgba(255,45,149,.6); }
.fc-glow-purple { text-shadow: 0 0 8px rgba(168,85,247,.6); }
.fc-glow-gold { text-shadow: 0 0 8px rgba(255,216,77,.6); }
.fc-glow-green { text-shadow: 0 0 8px rgba(57,255,122,.6); }
.fc-glow-red { text-shadow: 0 0 8px rgba(255,59,107,.6); }
/* App shell — wrap your <router-view> in <div class="fc-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;
overflow-x: hidden;
}
.fc-app::before {
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 {
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; }
/* Blinking utility for critical alerts (recurrence missing, etc.) */
@keyframes fc-blink { 0%,49% { opacity: 1 } 50%,100% { opacity: 0.2 } }
.fc-blink { animation: fc-blink 1.2s steps(1) infinite; }