From c327d5c9709930fe1fa6e2af7facc44238d4d4d2 Mon Sep 17 00:00:00 2001 From: Mlcarvalho1 Date: Tue, 26 May 2026 22:53:41 -0300 Subject: [PATCH] =?UTF-8?q?feat(#29):=20Arcade=20Neon=20design=20system=20?= =?UTF-8?q?=E2=80=94=20visual=20overhaul=20completo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementa o sistema visual Arcade Neon do neon-handoff/ em todo o app: - tokens.css: tokens --fc-*, utilities (fc-pixel/mono/body/glow), form inputs/buttons globais - AppHud.vue: HUD persistente com LV/XP/META + nav chips por rota - BottomNav.vue: nav mobile com 5 rotas - NeonPanel.vue, CharacterSprite.vue, XPBar.vue, HUDStat.vue: componentes base - MonthSwitcher.vue, LevelUpModal.vue - HomeView, CharacterView, TransactionsView, ImportView, CategoriesView, AccountsView, SettingsView: reescritos com Arcade Neon - sprites PNG (lv01–lv15) + sprite-data.json: cosméticos com temas de cor - game.ts: equippedTheme getter para merge de cores do cosmético equipado - 008_cosmetics.sql: seeds alinhados com sprite-data.json Co-Authored-By: Claude Sonnet 4.6 --- .../internal/migration/sql/008_cosmetics.sql | 21 +- apps/web/index.html | 3 + apps/web/src/App.vue | 48 +- apps/web/src/assets/icons.json | 67 ++ apps/web/src/assets/sprite-data.json | 160 +++++ .../src/assets/sprites/manoel-default-4x.png | Bin 0 -> 884 bytes .../web/src/assets/sprites/manoel-default.png | Bin 0 -> 472 bytes .../web/src/assets/sprites/manoel-lv01-4x.png | Bin 0 -> 874 bytes apps/web/src/assets/sprites/manoel-lv01.png | Bin 0 -> 465 bytes .../sprites/manoel-lv03-bone-verolme-4x.png | Bin 0 -> 883 bytes .../sprites/manoel-lv03-bone-verolme.png | Bin 0 -> 472 bytes .../sprites/manoel-lv05-wingspan-4x.png | Bin 0 -> 878 bytes .../assets/sprites/manoel-lv05-wingspan.png | Bin 0 -> 476 bytes .../sprites/manoel-lv07-chapeu-sol-4x.png | Bin 0 -> 884 bytes .../assets/sprites/manoel-lv07-chapeu-sol.png | Bin 0 -> 472 bytes .../sprites/manoel-lv10-anti-vento-4x.png | Bin 0 -> 872 bytes .../assets/sprites/manoel-lv10-anti-vento.png | Bin 0 -> 464 bytes .../assets/sprites/manoel-lv15-capitao-4x.png | Bin 0 -> 864 bytes .../assets/sprites/manoel-lv15-capitao.png | Bin 0 -> 455 bytes .../src/assets/sprites/manoel-sheet-4x.png | Bin 0 -> 3958 bytes apps/web/src/components/AppHud.vue | 117 ++++ apps/web/src/components/BottomNav.vue | 35 + apps/web/src/components/CharacterSprite.vue | 80 +++ apps/web/src/components/HUDStat.vue | 48 ++ apps/web/src/components/LevelUpModal.vue | 46 ++ apps/web/src/components/MonthSwitcher.vue | 34 + apps/web/src/components/NeonPanel.vue | 95 +++ apps/web/src/components/XPBar.vue | 57 +- apps/web/src/main.ts | 1 + apps/web/src/stores/game.ts | 32 +- apps/web/src/styles/tokens.css | 236 +++++++ apps/web/src/views/AccountsView.vue | 233 +++++-- apps/web/src/views/CategoriesView.vue | 254 +++++--- apps/web/src/views/CharacterView.vue | 597 +++++++++++++----- apps/web/src/views/HomeView.vue | 461 +++++++------- apps/web/src/views/ImportView.vue | 362 +++++++---- apps/web/src/views/SettingsView.vue | 195 ++++-- apps/web/src/views/TransactionsView.vue | 305 +++++---- 38 files changed, 2576 insertions(+), 911 deletions(-) create mode 100644 apps/web/src/assets/icons.json create mode 100644 apps/web/src/assets/sprite-data.json create mode 100644 apps/web/src/assets/sprites/manoel-default-4x.png create mode 100644 apps/web/src/assets/sprites/manoel-default.png create mode 100644 apps/web/src/assets/sprites/manoel-lv01-4x.png create mode 100644 apps/web/src/assets/sprites/manoel-lv01.png create mode 100644 apps/web/src/assets/sprites/manoel-lv03-bone-verolme-4x.png create mode 100644 apps/web/src/assets/sprites/manoel-lv03-bone-verolme.png create mode 100644 apps/web/src/assets/sprites/manoel-lv05-wingspan-4x.png create mode 100644 apps/web/src/assets/sprites/manoel-lv05-wingspan.png create mode 100644 apps/web/src/assets/sprites/manoel-lv07-chapeu-sol-4x.png create mode 100644 apps/web/src/assets/sprites/manoel-lv07-chapeu-sol.png create mode 100644 apps/web/src/assets/sprites/manoel-lv10-anti-vento-4x.png create mode 100644 apps/web/src/assets/sprites/manoel-lv10-anti-vento.png create mode 100644 apps/web/src/assets/sprites/manoel-lv15-capitao-4x.png create mode 100644 apps/web/src/assets/sprites/manoel-lv15-capitao.png create mode 100644 apps/web/src/assets/sprites/manoel-sheet-4x.png create mode 100644 apps/web/src/components/AppHud.vue create mode 100644 apps/web/src/components/BottomNav.vue create mode 100644 apps/web/src/components/CharacterSprite.vue create mode 100644 apps/web/src/components/HUDStat.vue create mode 100644 apps/web/src/components/LevelUpModal.vue create mode 100644 apps/web/src/components/MonthSwitcher.vue create mode 100644 apps/web/src/components/NeonPanel.vue create mode 100644 apps/web/src/styles/tokens.css 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 0000000000000000000000000000000000000000..03d72dc0fdd24bf1403c79c684808a1e6aec090b GIT binary patch literal 884 zcmV-)1B?8LP)*j@gvNMeHXMg&k887wQYs+B$1*8@T_oP2hQrmwRS1X) zh=70qY`8-B-C*){Y)x9Z@yO;W#zH>THVmXXV#@~*5G;^@M1hpf42+lA3(sDg*Z$uv zYm)Dq+S!1(kew-$>C~J!e>|R9RS1v>h=70qY^so%OWbqW9-ZmkJ(B_HUW&1d1O&VU zL_k0QRuv*3AONchi2_OACu)RvcJam4MzMcQ?DQMR_{vJ9{~ZpB9PK=l7qe9)+EHU(s-r& zO|jCwXgu~W0^tK)_X2_y77zeA1ONd4|98SWVgLXD21!IgR09AX4?N6NWt{#10000< KMNUMnLSTaF!<0M# literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a391da24922e1666d549053bd1a407625cf9c0e9 GIT binary patch literal 472 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 Ef&wvw5dZ)H literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c1615093e5be076c2a182a454b6a002c4ec292b0 GIT binary patch literal 476 zcmV<20VDp2P)o`FAfQbxB^$)@HtpWJG+34ZcmbWf|tRFbzSpL2Hz)=4B3F z-3m6Vz`Wiho1Vz;#$IKZ<6sSGb+2SqG~_*ds)ZoVM|~*+PdKADK?I(UVd^sQ;5CmA zL8M8eeK3&O{R3qL8ofw#8z16o|0b-nzw7r_pwT0U$1yz$;UpTEE(bS~f{uqEZjKjx z2+6&Ru|5W-)Bd5PjL&+^2x0hj<34QL(UAzV2GW8?f*fI93^qZt~) zx4kx+$%b-zNp&ILcsO(ZxF8N5Ngza00030|1tCgG5`Po21!IgR09Cw4^=QA ST|8X?0000*j@gvNMeHXMg&k887wQYs+B$1*8@T_oP2hQrmwRS1X) zh=70qY`8-B-C*){Y)x9Z@yO;W#zH>THVmXXV#@~*5G;^@M1hpf42+lA3(sDg*Z$uv zYm)Dq+S!1(kew-$>C~J!e>|R9RS1v>h=70qY^so%OWbqW9-ZmkJ(B_HUW&1d1O&VU zL_k0QRuv*3AONchi2_OACu)RvcJam4MzMcQ?DQMR_{vJ9{~ZpB9PK=l7qe9)+EHU(s-r& zO|jCwXgu~W0^tK)_X2_y77zeA1ONd4|98SWVgLXD21!IgR09AX4?N6NWt{#10000< KMNUMnLSTaF!<0M# literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a391da24922e1666d549053bd1a407625cf9c0e9 GIT binary patch literal 472 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 Uq@$}NhibL0Lo zbug4wrK-Hqz}aX?Hb5Y9x&F@EB=GgDuJ~+Lg|w!Jn0fu)8wYkEKYIBcApU^7@E^}! BUZem3 literal 0 HcmV?d00001 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)