diff --git a/apps/api/internal/migration/sql/008_cosmetics.sql b/apps/api/internal/migration/sql/008_cosmetics.sql index aae41e8..746762f 100644 --- a/apps/api/internal/migration/sql/008_cosmetics.sql +++ b/apps/api/internal/migration/sql/008_cosmetics.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS cosmetics ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL UNIQUE, - type VARCHAR(20) NOT NULL CHECK (type IN ('hair','shirt','pants','shoes','hat','accessory')), + type VARCHAR(20) NOT NULL CHECK (type IN ('outfit','hat','accessory')), unlock_level INTEGER NOT NULL, css_data JSONB NOT NULL DEFAULT '{}' ); @@ -13,13 +13,20 @@ CREATE TABLE IF NOT EXISTS player_cosmetics ( unlocked_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() ); +-- Seeds aligned with sprite-data.json themes (keys = sprite color slots) INSERT INTO cosmetics (name, type, unlock_level, css_data) VALUES - ('Cabelo Ruivo', 'hair', 2, '{"color":"#8B1a0a"}'), - ('Camisa Marinheiro', 'shirt', 3, '{"color":"#1e3a5f"}'), - ('Calças Aventureiro', 'pants', 4, '{"color":"#2d4a1e"}'), - ('Botas de Capitão', 'shoes', 5, '{"color":"#8B4513"}'), - ('Camisa Dourada', 'shirt', 7, '{"color":"#d4af37"}'), - ('Cabelo Platinado', 'hair', 10, '{"color":"#e8e8e8"}') + ('Tripulante Iniciante', 'outfit', 1, + '{"sprite_id":"lv01-tripulante","h":"#5a3a1d","w":"#5a3a1d","c":"#0ea5e9","C":"#075985","p":"#a16207","P":"#7c4a08","b":"#1e293b"}'), + ('Boné Verolme', 'outfit', 3, + '{"sprite_id":"lv03-bone-verolme","h":"#1e40af","w":"#fde68a","c":"#0ea5e9","C":"#075985","p":"#a16207","P":"#7c4a08","b":"#1e293b"}'), + ('Camiseta Wingspan', 'outfit', 5, + '{"sprite_id":"lv05-wingspan","h":"#ffd84d","w":"#fde68a","c":"#10b981","C":"#065f46","p":"#3a1f6e","P":"#2d1857","b":"#0f172a"}'), + ('Chapéu de Sol', 'outfit', 7, + '{"sprite_id":"lv07-chapeu-sol","h":"#ffd84d","w":"#fde68a","c":"#00f0ff","C":"#0369a1","p":"#3a1f6e","P":"#2d1857","b":"#0f172a"}'), + ('Casaco Anti-Vento', 'outfit', 10, + '{"sprite_id":"lv10-anti-vento","h":"#1e40af","w":"#fde68a","c":"#a855f7","C":"#581c87","p":"#1e293b","P":"#0f172a","b":"#0f172a"}'), + ('Trajado de Capitão', 'outfit', 15, + '{"sprite_id":"lv15-capitao","h":"#ffd84d","w":"#dc2626","c":"#dc2626","C":"#7a0e0e","p":"#1c1917","P":"#0c0a09","b":"#0c0a09"}') ON CONFLICT (name) DO NOTHING; INSERT INTO schema_migrations (version) VALUES (8) ON CONFLICT DO NOTHING; diff --git a/apps/web/index.html b/apps/web/index.html index d5a4d0e..8349d8f 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -5,6 +5,9 @@ Financeiro Carvalho + + +
diff --git a/apps/web/src/App.vue b/apps/web/src/App.vue index b198a92..a8ccf30 100644 --- a/apps/web/src/App.vue +++ b/apps/web/src/App.vue @@ -1,39 +1,29 @@ - - diff --git a/apps/web/src/assets/icons.json b/apps/web/src/assets/icons.json new file mode 100644 index 0000000..d157d42 --- /dev/null +++ b/apps/web/src/assets/icons.json @@ -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)" + } +} diff --git a/apps/web/src/assets/sprite-data.json b/apps/web/src/assets/sprite-data.json new file mode 100644 index 0000000..5319030 --- /dev/null +++ b/apps/web/src/assets/sprite-data.json @@ -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" + } + } + ] +} diff --git a/apps/web/src/assets/sprites/manoel-default-4x.png b/apps/web/src/assets/sprites/manoel-default-4x.png new file mode 100644 index 0000000..03d72dc Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-default-4x.png differ diff --git a/apps/web/src/assets/sprites/manoel-default.png b/apps/web/src/assets/sprites/manoel-default.png new file mode 100644 index 0000000..a391da2 Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-default.png differ diff --git a/apps/web/src/assets/sprites/manoel-lv01-4x.png b/apps/web/src/assets/sprites/manoel-lv01-4x.png new file mode 100644 index 0000000..5be578e Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv01-4x.png differ diff --git a/apps/web/src/assets/sprites/manoel-lv01.png b/apps/web/src/assets/sprites/manoel-lv01.png new file mode 100644 index 0000000..c010162 Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv01.png differ diff --git a/apps/web/src/assets/sprites/manoel-lv03-bone-verolme-4x.png b/apps/web/src/assets/sprites/manoel-lv03-bone-verolme-4x.png new file mode 100644 index 0000000..fcbae5e Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv03-bone-verolme-4x.png differ diff --git a/apps/web/src/assets/sprites/manoel-lv03-bone-verolme.png b/apps/web/src/assets/sprites/manoel-lv03-bone-verolme.png new file mode 100644 index 0000000..c679f2d Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv03-bone-verolme.png differ diff --git a/apps/web/src/assets/sprites/manoel-lv05-wingspan-4x.png b/apps/web/src/assets/sprites/manoel-lv05-wingspan-4x.png new file mode 100644 index 0000000..b92a448 Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv05-wingspan-4x.png differ diff --git a/apps/web/src/assets/sprites/manoel-lv05-wingspan.png b/apps/web/src/assets/sprites/manoel-lv05-wingspan.png new file mode 100644 index 0000000..c161509 Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv05-wingspan.png differ diff --git a/apps/web/src/assets/sprites/manoel-lv07-chapeu-sol-4x.png b/apps/web/src/assets/sprites/manoel-lv07-chapeu-sol-4x.png new file mode 100644 index 0000000..03d72dc Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv07-chapeu-sol-4x.png differ diff --git a/apps/web/src/assets/sprites/manoel-lv07-chapeu-sol.png b/apps/web/src/assets/sprites/manoel-lv07-chapeu-sol.png new file mode 100644 index 0000000..a391da2 Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv07-chapeu-sol.png differ diff --git a/apps/web/src/assets/sprites/manoel-lv10-anti-vento-4x.png b/apps/web/src/assets/sprites/manoel-lv10-anti-vento-4x.png new file mode 100644 index 0000000..9143d46 Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv10-anti-vento-4x.png differ diff --git a/apps/web/src/assets/sprites/manoel-lv10-anti-vento.png b/apps/web/src/assets/sprites/manoel-lv10-anti-vento.png new file mode 100644 index 0000000..9938031 Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv10-anti-vento.png differ diff --git a/apps/web/src/assets/sprites/manoel-lv15-capitao-4x.png b/apps/web/src/assets/sprites/manoel-lv15-capitao-4x.png new file mode 100644 index 0000000..9a2ef2c Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv15-capitao-4x.png differ diff --git a/apps/web/src/assets/sprites/manoel-lv15-capitao.png b/apps/web/src/assets/sprites/manoel-lv15-capitao.png new file mode 100644 index 0000000..04f5295 Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv15-capitao.png differ diff --git a/apps/web/src/assets/sprites/manoel-sheet-4x.png b/apps/web/src/assets/sprites/manoel-sheet-4x.png new file mode 100644 index 0000000..94dd71c Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-sheet-4x.png differ diff --git a/apps/web/src/components/AppHud.vue b/apps/web/src/components/AppHud.vue new file mode 100644 index 0000000..2be94e0 --- /dev/null +++ b/apps/web/src/components/AppHud.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/apps/web/src/components/BottomNav.vue b/apps/web/src/components/BottomNav.vue new file mode 100644 index 0000000..c6c0edc --- /dev/null +++ b/apps/web/src/components/BottomNav.vue @@ -0,0 +1,35 @@ + + + diff --git a/apps/web/src/components/CharacterSprite.vue b/apps/web/src/components/CharacterSprite.vue new file mode 100644 index 0000000..5a79d73 --- /dev/null +++ b/apps/web/src/components/CharacterSprite.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/apps/web/src/components/HUDStat.vue b/apps/web/src/components/HUDStat.vue new file mode 100644 index 0000000..14ba543 --- /dev/null +++ b/apps/web/src/components/HUDStat.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/apps/web/src/components/LevelUpModal.vue b/apps/web/src/components/LevelUpModal.vue new file mode 100644 index 0000000..0245068 --- /dev/null +++ b/apps/web/src/components/LevelUpModal.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/apps/web/src/components/MonthSwitcher.vue b/apps/web/src/components/MonthSwitcher.vue new file mode 100644 index 0000000..9ae334b --- /dev/null +++ b/apps/web/src/components/MonthSwitcher.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/apps/web/src/components/NeonPanel.vue b/apps/web/src/components/NeonPanel.vue new file mode 100644 index 0000000..08d7326 --- /dev/null +++ b/apps/web/src/components/NeonPanel.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/apps/web/src/components/XPBar.vue b/apps/web/src/components/XPBar.vue index 4b084ad..603ac13 100644 --- a/apps/web/src/components/XPBar.vue +++ b/apps/web/src/components/XPBar.vue @@ -1,42 +1,45 @@ diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index f668e8c..c44958a 100644 --- a/apps/web/src/main.ts +++ b/apps/web/src/main.ts @@ -1,6 +1,7 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' +import './styles/tokens.css' import App from './App.vue' import router from './router' diff --git a/apps/web/src/stores/game.ts b/apps/web/src/stores/game.ts index d32bf20..ceb020e 100644 --- a/apps/web/src/stores/game.ts +++ b/apps/web/src/stores/game.ts @@ -1,5 +1,6 @@ import { defineStore } from 'pinia' -import { ref } from 'vue' +import { ref, computed } from 'vue' +import spriteData from '@/assets/sprite-data.json' export interface PlayerProfile { id: number @@ -76,5 +77,32 @@ export const useGameStore = defineStore('game', () => { await fetchSummary() } - return { summary, loading, fetchSummary, claimQuest, equipCosmetic } + // Merge sprite theme from equipped cosmetics. + // css_data JSON may contain a "sprite_id" matching sprite-data.json cosmetics, + // OR the full theme directly. Falls back to empty (default sprite colors). + const equippedTheme = computed>(() => { + if (!summary.value) return {} + const equipped = summary.value.equipped_cosmetics + if (!equipped.length) return {} + return equipped.reduce((acc, c) => { + try { + const data = JSON.parse(c.css_data) + // If css_data has sprite_id, look up in sprite-data.json + if (data.sprite_id) { + const spr = (spriteData.cosmetics as any[]).find(x => x.id === data.sprite_id) + if (spr?.theme) return { ...acc, ...spr.theme } + } + // Otherwise treat css_data itself as theme (keys h/c/p/b etc.) + const themeKeys = ['h','w','c','C','p','P','b'] + const direct = Object.fromEntries( + Object.entries(data).filter(([k]) => themeKeys.includes(k)) + ) as Record + return { ...acc, ...direct } + } catch { return acc } + }, {} as Record) + }) + + const justLeveledUp = ref(false) + + return { summary, loading, equippedTheme, justLeveledUp, fetchSummary, claimQuest, equipCosmetic } }) diff --git a/apps/web/src/styles/tokens.css b/apps/web/src/styles/tokens.css new file mode 100644 index 0000000..1c5cc83 --- /dev/null +++ b/apps/web/src/styles/tokens.css @@ -0,0 +1,236 @@ +/* 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 in
*/ +.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; } + +/* ── Form elements ───────────────────────────────────────────── */ +.fc-input, +.fc-select { + font-family: var(--fc-font-mono); + font-size: 12px; + padding: 8px 10px; + background: var(--fc-bg); + border: 1px solid var(--fc-panel-edge); + border-radius: var(--fc-radius); + color: var(--fc-text); + outline: none; + transition: border-color .15s, box-shadow .15s; + width: 100%; +} +.fc-input::placeholder { color: var(--fc-text-dim); } +.fc-input:focus, +.fc-select:focus { + border-color: var(--fc-accent-3); + box-shadow: 0 0 8px rgba(168,85,247,.3); +} +.fc-select { + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%238b75b8'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 10px center; + padding-right: 28px; +} +.fc-select option { background: var(--fc-bg-panel); color: var(--fc-text); } + +.fc-label { + font-family: var(--fc-font-mono); + font-size: 10px; + color: var(--fc-text-dim); + display: flex; + flex-direction: column; + gap: 4px; +} + +/* ── Buttons ─────────────────────────────────────────────────── */ +.fc-btn { + font-family: var(--fc-font-pixel); + font-size: 8px; + letter-spacing: .04em; + padding: 8px 14px; + border: 2px solid var(--fc-panel-edge); + border-radius: var(--fc-radius); + background: var(--fc-bg-raised); + color: var(--fc-text); + cursor: pointer; + transition: border-color .12s, box-shadow .12s, background .12s; + white-space: nowrap; +} +.fc-btn:disabled { opacity: .45; cursor: not-allowed; } + +.fc-btn--primary { + border-color: var(--fc-accent-3); + color: var(--fc-accent-3); +} +.fc-btn--primary:not(:disabled):hover { + background: var(--fc-accent-3); + color: var(--fc-bg); + box-shadow: 0 0 14px rgba(168,85,247,.5); +} + +.fc-btn--pink { + border-color: var(--fc-accent); + color: var(--fc-accent); +} +.fc-btn--pink:not(:disabled):hover { + background: var(--fc-accent); + color: #fff; + box-shadow: 0 0 14px rgba(255,45,149,.5); +} + +.fc-btn--ghost { + border-color: transparent; + color: var(--fc-text-dim); +} +.fc-btn--ghost:hover { color: var(--fc-text); border-color: var(--fc-panel-edge); } + +.fc-btn--danger { + border-color: var(--fc-red); + color: var(--fc-red); +} +.fc-btn--danger:not(:disabled):hover { + background: var(--fc-red); + color: #fff; + box-shadow: 0 0 14px rgba(255,59,107,.5); +} + +.fc-btn--sm { + font-size: 7px; + padding: 5px 10px; +} diff --git a/apps/web/src/views/AccountsView.vue b/apps/web/src/views/AccountsView.vue index b4d564e..0efff24 100644 --- a/apps/web/src/views/AccountsView.vue +++ b/apps/web/src/views/AccountsView.vue @@ -1,9 +1,9 @@ diff --git a/apps/web/src/views/CategoriesView.vue b/apps/web/src/views/CategoriesView.vue index 7bdacf1..372a32e 100644 --- a/apps/web/src/views/CategoriesView.vue +++ b/apps/web/src/views/CategoriesView.vue @@ -1,11 +1,12 @@ diff --git a/apps/web/src/views/CharacterView.vue b/apps/web/src/views/CharacterView.vue index cedc268..396bd73 100644 --- a/apps/web/src/views/CharacterView.vue +++ b/apps/web/src/views/CharacterView.vue @@ -1,125 +1,155 @@ diff --git a/apps/web/src/views/HomeView.vue b/apps/web/src/views/HomeView.vue index 238f47a..c8e6de0 100644 --- a/apps/web/src/views/HomeView.vue +++ b/apps/web/src/views/HomeView.vue @@ -2,260 +2,297 @@ import { ref, computed, onMounted } from 'vue' import { useDashboardStore } from '@/stores/dashboard' import { useGameStore } from '@/stores/game' +import NeonPanel from '@/components/NeonPanel.vue' import XPBar from '@/components/XPBar.vue' +import CharacterSprite from '@/components/CharacterSprite.vue' +import MonthSwitcher from '@/components/MonthSwitcher.vue' -const store = useDashboardStore() -const gameStore = useGameStore() +const dash = useDashboardStore() +const game = useGameStore() const currentMonth = ref(new Date().toISOString().slice(0, 7)) -onMounted(() => { - store.fetch(currentMonth.value) - gameStore.fetchSummary() -}) +onMounted(() => dash.fetch(currentMonth.value)) -function changeMonth(delta: number) { +function prev() { const [y, m] = currentMonth.value.split('-').map(Number) - const d = new Date(y, m - 1 + delta, 1) + const d = new Date(y, m - 2, 1) currentMonth.value = d.toISOString().slice(0, 7) - store.fetch(currentMonth.value) + dash.fetch(currentMonth.value) +} +function next() { + const [y, m] = currentMonth.value.split('-').map(Number) + const d = new Date(y, m, 1) + currentMonth.value = d.toISOString().slice(0, 7) + dash.fetch(currentMonth.value) } function fmt(v: number) { return v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }) } -function fmtMonth(m: string) { - const [y, mo] = m.split('-').map(Number) - return new Date(y, mo - 1, 1).toLocaleString('pt-BR', { month: 'short', year: 'numeric' }) -} - -const maxCategoryTotal = computed(() => { - if (!store.data?.by_category.length) return 1 - return Math.max(...store.data.by_category.map((c) => c.total)) +const stageLabel = computed(() => { + const [y, m] = currentMonth.value.split('-').map(Number) + const name = new Date(y, m - 1, 1).toLocaleString('pt-BR', { month: 'long' }).toUpperCase() + return `:: STAGE ${m.toString().padStart(2,'0')} :: ${name} · ${y}` }) -const maxEvolution = computed(() => { - if (!store.data?.monthly_evolution.length) return 1 - return Math.max(...store.data.monthly_evolution.flatMap((m) => [m.income, m.expenses])) +const maxCat = computed(() => { + if (!dash.data?.by_category.length) return 1 + return Math.max(...dash.data.by_category.map(c => c.total)) }) +const maxEvo = computed(() => { + if (!dash.data?.monthly_evolution.length) return 1 + return Math.max(...dash.data.monthly_evolution.flatMap(m => [m.income, m.expenses])) +}) +const xpPct = computed(() => { + const p = game.summary?.profile + if (!p) return 0 + return Math.round((p.xp / p.xp_to_next) * 100) +}) +const savingsPct = computed(() => dash.data?.savings_pct ?? 0) +const savingsOk = computed(() => savingsPct.value >= 40)