diff --git a/src/features/home/HomePage.tsx b/src/features/home/HomePage.tsx index 3935154..dd9dd42 100644 --- a/src/features/home/HomePage.tsx +++ b/src/features/home/HomePage.tsx @@ -6,9 +6,9 @@ const SnakeGame = lazy(() => export function HomePage() { return ( -
-
-
+
+
+

diff --git a/src/features/snake-game/SnakeGame.tsx b/src/features/snake-game/SnakeGame.tsx index 5dcabf5..3e6d657 100644 --- a/src/features/snake-game/SnakeGame.tsx +++ b/src/features/snake-game/SnakeGame.tsx @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import './styles/snake-game.css'; import { useSnakeGame } from './hooks/useSnakeGame'; import { BOLT_VARIANTS } from './constants'; import type { Direction } from './types'; @@ -52,7 +53,7 @@ export function SnakeGame({ className = '' }: SnakeGameProps) { return (

-
+
-
@@ -123,10 +121,7 @@ export function SnakeGame({ className = '' }: SnakeGameProps) {
-
diff --git a/src/features/snake-game/components/Countdown.tsx b/src/features/snake-game/components/Countdown.tsx index d690b6a..bdc447d 100644 --- a/src/features/snake-game/components/Countdown.tsx +++ b/src/features/snake-game/components/Countdown.tsx @@ -23,8 +23,8 @@ export function Countdown({ onFinish }: CountdownProps) {
{value === 'go' ? 'GO!' : value}
diff --git a/src/features/snake-game/components/GameControls.tsx b/src/features/snake-game/components/GameControls.tsx index 8c33008..0399cc2 100644 --- a/src/features/snake-game/components/GameControls.tsx +++ b/src/features/snake-game/components/GameControls.tsx @@ -18,7 +18,7 @@ export const GameControls = memo(function GameControls({ status, onDirection }: const disabled = status !== 'playing'; const buttonClass = - 'bg-[#0a0a0a] border border-[#314158] hover:border-[#43D9AD] disabled:hover:border-[#314158] disabled:opacity-50 transition-colors rounded-lg w-11 h-11 sm:w-10 sm:h-10 flex items-center justify-center'; + 'snake-arrow-btn w-11 h-11 sm:w-10 sm:h-10 flex items-center justify-center'; return (
diff --git a/src/features/snake-game/components/GameOverlay.tsx b/src/features/snake-game/components/GameOverlay.tsx index c37ee97..f52fc99 100644 --- a/src/features/snake-game/components/GameOverlay.tsx +++ b/src/features/snake-game/components/GameOverlay.tsx @@ -53,7 +53,7 @@ export const GameOverlay = memo(function GameOverlay({

{isCompetitive && ( -

+

pontos: {score}

)} @@ -76,7 +76,7 @@ export const GameOverlay = memo(function GameOverlay({ @@ -136,10 +136,7 @@ export const GameOverlay = memo(function GameOverlay({
)} -
diff --git a/src/features/snake-game/components/GameTabs.tsx b/src/features/snake-game/components/GameTabs.tsx index 54e65d2..7d9732f 100644 --- a/src/features/snake-game/components/GameTabs.tsx +++ b/src/features/snake-game/components/GameTabs.tsx @@ -21,32 +21,24 @@ export const GameTabs = memo(function GameTabs({ onMode, }: GameTabsProps) { return ( -
+
{DIFFICULTIES.map((d) => ( ))} -
+
{MODES.map((m) => ( diff --git a/src/features/snake-game/components/HighScoreBadge.tsx b/src/features/snake-game/components/HighScoreBadge.tsx index 0c0d0ad..49d29a5 100644 --- a/src/features/snake-game/components/HighScoreBadge.tsx +++ b/src/features/snake-game/components/HighScoreBadge.tsx @@ -4,7 +4,7 @@ interface HighScoreBadgeProps { export function HighScoreBadge({ highScore }: HighScoreBadgeProps) { return ( -
+
best: {highScore}
); diff --git a/src/features/snake-game/components/PauseOverlay.tsx b/src/features/snake-game/components/PauseOverlay.tsx index f881bcc..5cbfb87 100644 --- a/src/features/snake-game/components/PauseOverlay.tsx +++ b/src/features/snake-game/components/PauseOverlay.tsx @@ -9,23 +9,17 @@ export const PauseOverlay = memo(function PauseOverlay({ onResume, onQuit }: Pau return (
-

+

pausado

// pressione espaco para continuar

- -
diff --git a/src/features/snake-game/constants/index.ts b/src/features/snake-game/constants/index.ts index fe913b6..47b97ce 100644 --- a/src/features/snake-game/constants/index.ts +++ b/src/features/snake-game/constants/index.ts @@ -23,7 +23,7 @@ export const DIFFICULTY_LABELS: Record = { export const FOOD_TYPES: Record = { function: { points: 1, color: '#46ECD5', label: 'function()', weight: 70 }, - class: { points: 2, color: '#ffb86a', label: 'class{}', weight: 22 }, + class: { points: 2, color: '#ffa1ad', label: 'class{}', weight: 22 }, async: { points: 3, color: '#b14eff', label: 'async()', weight: 8 }, }; diff --git a/src/features/snake-game/styles/snake-game.css b/src/features/snake-game/styles/snake-game.css new file mode 100644 index 0000000..d5b8741 --- /dev/null +++ b/src/features/snake-game/styles/snake-game.css @@ -0,0 +1,296 @@ +/* ================================================================ + Snake Game styles + Organized by: tokens > buttons > tabs > arrows > combo > animations + ================================================================ */ + +:root { + --sg-font: 'Fira Code', monospace; + --sg-radius: 0.5rem; + --sg-radius-sm: 0.375rem; + --sg-transition: 150ms ease-out; + --sg-transition-slow: 180ms ease-out; + --sg-transition-fast: 120ms ease-out; + + --sg-color-text: #f8fafc; + --sg-color-muted: #90a1b9; + --sg-color-border: #314158; + --sg-color-dark: #020618; + + --sg-green-light: #7eedcf; + --sg-green: #43d9ad; + --sg-green-deep: #2eb38e; + --sg-purple-light: #c490f7; + --sg-purple: #9d4edd; + --sg-purple-deep: #7c3aed; + --sg-purple-glow: rgba(157, 78, 221, 0.5); + --sg-pink: #b14eff; +} + +/* ---------------------------------------------------------------- + Base button (snake-btn) + Shared spec for primary / success / ghost variants. + ---------------------------------------------------------------- */ +.snake-btn { + font-family: var(--sg-font); + font-weight: 500; + font-size: 0.875rem; + letter-spacing: 0.02em; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.625rem 1.25rem; + border-radius: var(--sg-radius); + border: 1px solid transparent; + cursor: pointer; + white-space: nowrap; + transition: + transform var(--sg-transition-fast), + box-shadow var(--sg-transition-slow), + background var(--sg-transition-slow), + border-color var(--sg-transition-slow); +} + +.snake-btn::before { + content: '>'; + font-weight: 600; + opacity: 0.7; + transition: transform var(--sg-transition-slow), opacity var(--sg-transition-slow); +} + +.snake-btn:hover::before { transform: translateX(2px); opacity: 1; } +.snake-btn:active { transform: translateY(1px); } +.snake-btn:focus-visible { outline: 2px solid var(--sg-color-text); outline-offset: 3px; } +.snake-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; } + +.snake-btn-compact { padding: 0.4rem 0.9rem; font-size: 0.75rem; } + +/* variant: primary (purple) */ +.snake-btn-primary { + background: linear-gradient(180deg, var(--sg-purple-light) 0%, var(--sg-purple) 60%, var(--sg-purple-deep) 100%); + color: #f8fafc; + border-color: rgba(255, 255, 255, 0.25); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.35), + 0 2px 0 rgba(60, 20, 100, 0.5), + 0 0 22px -4px var(--sg-purple-glow); +} +.snake-btn-primary:hover { + background: linear-gradient(180deg, #d4a8fa 0%, #af6ae7 60%, #8b50e8 100%); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.4), + 0 3px 0 rgba(60, 20, 100, 0.5), + 0 0 36px -4px rgba(157, 78, 221, 0.75); + transform: translateY(-1px); +} + +/* variant: success (green) */ +.snake-btn-success { + background: linear-gradient(180deg, var(--sg-green-light) 0%, var(--sg-green) 60%, var(--sg-green-deep) 100%); + color: #04201a; + border-color: rgba(255, 255, 255, 0.25); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.5), + 0 2px 0 rgba(10, 80, 60, 0.5), + 0 0 22px -4px rgba(67, 217, 173, 0.5); +} +.snake-btn-success:hover { + background: linear-gradient(180deg, #9af3dc 0%, #5ee2bb 60%, #3fc49d 100%); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.55), + 0 3px 0 rgba(10, 80, 60, 0.5), + 0 0 34px -4px rgba(67, 217, 173, 0.75); + transform: translateY(-1px); +} + +/* variant: ghost (outline) */ +.snake-btn-ghost { + background: rgba(148, 163, 184, 0.04); + color: var(--sg-color-text); + border-color: rgba(148, 163, 184, 0.35); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); +} +.snake-btn-ghost::before { opacity: 0.5; } +.snake-btn-ghost:hover { + background: rgba(148, 163, 184, 0.12); + border-color: rgba(248, 250, 252, 0.7); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 0 18px -6px rgba(248, 250, 252, 0.3); + transform: translateY(-1px); +} + +/* ---------------------------------------------------------------- + Tabs (difficulty / mode) + ---------------------------------------------------------------- */ +.snake-tab { + font-family: var(--sg-font); + font-size: 0.75rem; + padding: 0.35rem 0.7rem; + border-radius: var(--sg-radius-sm); + border: 1px solid transparent; + color: var(--sg-color-muted); + cursor: pointer; + transition: + color var(--sg-transition), + background var(--sg-transition), + border-color var(--sg-transition), + box-shadow var(--sg-transition-slow); +} +.snake-tab:hover:not(:disabled) { color: var(--sg-color-text); background: rgba(148, 163, 184, 0.06); } +.snake-tab:disabled { opacity: 0.4; cursor: not-allowed; } +.snake-tab:focus-visible { outline: 2px solid var(--sg-green); outline-offset: 2px; } + +.snake-tab-active-green { + background: linear-gradient(180deg, #5de3b8 0%, var(--sg-green) 100%); + color: #04201a; + border-color: rgba(255, 255, 255, 0.25); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.4), + 0 0 16px -4px rgba(67, 217, 173, 0.6); +} +.snake-tab-active-green:hover:not(:disabled) { + background: linear-gradient(180deg, #7aeccb 0%, #5ee2bb 100%); + color: #04201a; +} + +.snake-tab-active-purple { + background: linear-gradient(180deg, var(--sg-purple-light) 0%, var(--sg-purple) 100%); + color: var(--sg-color-text); + border-color: rgba(255, 255, 255, 0.2); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.3), + 0 0 16px -4px rgba(157, 78, 221, 0.6); +} +.snake-tab-active-purple:hover:not(:disabled) { + background: linear-gradient(180deg, #c68af5 0%, #ae68e3 100%); + color: var(--sg-color-text); +} + +/* ---------------------------------------------------------------- + Arrow buttons (directional controls) + ---------------------------------------------------------------- */ +.snake-arrow-btn { + background: linear-gradient(180deg, #111a2e 0%, #0a0f1e 100%); + border: 1px solid var(--sg-color-border); + border-radius: var(--sg-radius); + cursor: pointer; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); + transition: + border-color var(--sg-transition), + box-shadow var(--sg-transition-slow), + transform var(--sg-transition-fast), + background var(--sg-transition); +} +.snake-arrow-btn:hover:not(:disabled) { + border-color: var(--sg-green); + background: linear-gradient(180deg, #152238 0%, #0e1524 100%); + box-shadow: + inset 0 1px 0 rgba(67, 217, 173, 0.25), + 0 0 14px -4px rgba(67, 217, 173, 0.5); +} +.snake-arrow-btn:active:not(:disabled) { transform: translateY(1px); } +.snake-arrow-btn:disabled { opacity: 0.45; cursor: not-allowed; } +.snake-arrow-btn:focus-visible { outline: 2px solid var(--sg-green); outline-offset: 2px; } + +/* ---------------------------------------------------------------- + Combo bar (fills during combo window) + ---------------------------------------------------------------- */ +.snake-combo { + animation: snake-combo-fade linear forwards; +} + +.snake-combo-bar { + display: inline-block; + width: 40px; + height: 3px; + background: var(--sg-pink); + border-radius: 2px; + transform-origin: left center; + animation: snake-combo-bar linear forwards; +} + +/* ---------------------------------------------------------------- + Keyframes + ---------------------------------------------------------------- */ +@keyframes snake-shake { + 0%, 100% { transform: translate(0, 0); } + 20% { transform: translate(-2px, 1px); } + 40% { transform: translate(2px, -1px); } + 60% { transform: translate(-1px, 2px); } + 80% { transform: translate(1px, -2px); } +} + +@keyframes snake-food-pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.15); } +} + +@keyframes snake-particle-burst { + 0% { transform: translate(0, 0) scale(1); opacity: 1; } + 100% { transform: translate(var(--px, 0), var(--py, 0)) scale(0.2); opacity: 0; } +} + +@keyframes combo-pop { + 0% { transform: scale(0.7); opacity: 0; } + 50% { transform: scale(1.15); } + 100% { transform: scale(1); opacity: 1; } +} + +@keyframes snake-combo-fade { + 0%, 60% { opacity: 1; } + 100% { opacity: 0; } +} + +@keyframes snake-combo-bar { + 0% { transform: scaleX(1); } + 100% { transform: scaleX(0); } +} + +@keyframes countdown-pop { + 0% { transform: scale(0.4); opacity: 0; } + 40% { transform: scale(1.1); opacity: 1; } + 100% { transform: scale(1); opacity: 1; } +} + +/* ---------------------------------------------------------------- + Utilities (attach animations to elements) + ---------------------------------------------------------------- */ +.snake-shake { animation: snake-shake 0.18s ease-in-out; } +.snake-food-pulse { + animation: snake-food-pulse 1.2s ease-in-out infinite; + transform-box: fill-box; +} +.snake-particle { + animation-name: snake-particle-burst; + animation-timing-function: ease-out; + animation-fill-mode: forwards; + transform-box: fill-box; + transform-origin: center; +} +.combo-pop { animation: combo-pop 0.25s ease-out; } +.countdown-pop { animation: countdown-pop 0.3s ease-out; } + +/* ---------------------------------------------------------------- + Reduced motion + ---------------------------------------------------------------- */ +@media (prefers-reduced-motion: reduce) { + .snake-shake, + .snake-food-pulse, + .snake-particle, + .combo-pop, + .countdown-pop, + .snake-combo, + .snake-combo-bar { + animation: none !important; + } + + .snake-btn, + .snake-btn::before, + .snake-tab, + .snake-arrow-btn { + transition: none !important; + transform: none !important; + } +} diff --git a/src/styles/theme.css b/src/styles/theme.css index 66b7846..9b833a1 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -54,92 +54,6 @@ animation: tab-fade-in 0.2s ease-out; } -@keyframes snake-shake { - 0%, 100% { transform: translate(0, 0); } - 20% { transform: translate(-2px, 1px); } - 40% { transform: translate(2px, -1px); } - 60% { transform: translate(-1px, 2px); } - 80% { transform: translate(1px, -2px); } -} - -.snake-shake { - animation: snake-shake 0.18s ease-in-out; -} - -@keyframes snake-food-pulse { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.15); } -} - -.snake-food-pulse { - animation: snake-food-pulse 1.2s ease-in-out infinite; - transform-box: fill-box; -} - -@keyframes snake-particle-burst { - 0% { - transform: translate(0, 0) scale(1); - opacity: 1; - } - 100% { - transform: translate(var(--px, 0), var(--py, 0)) scale(0.2); - opacity: 0; - } -} - -.snake-particle { - animation-name: snake-particle-burst; - animation-timing-function: ease-out; - animation-fill-mode: forwards; - transform-box: fill-box; - transform-origin: center; -} - -@keyframes combo-pop { - 0% { transform: scale(0.7); opacity: 0; } - 50% { transform: scale(1.15); } - 100% { transform: scale(1); opacity: 1; } -} - -.combo-pop { - animation: combo-pop 0.25s ease-out; -} - -@keyframes snake-combo-fade { - 0% { opacity: 1; } - 60% { opacity: 1; } - 100% { opacity: 0; } -} - -.snake-combo { - animation: snake-combo-fade linear forwards; -} - -@keyframes snake-combo-bar { - 0% { transform: scaleX(1); } - 100% { transform: scaleX(0); } -} - -.snake-combo-bar { - display: inline-block; - width: 40px; - height: 3px; - background: #b14eff; - border-radius: 2px; - transform-origin: left center; - animation: snake-combo-bar linear forwards; -} - -@keyframes countdown-pop { - 0% { transform: scale(0.4); opacity: 0; } - 40% { transform: scale(1.1); opacity: 1; } - 100% { transform: scale(1); opacity: 1; } -} - -.countdown-pop { - animation: countdown-pop 0.3s ease-out; -} - @media (prefers-reduced-motion: reduce) { *, *::before, @@ -150,10 +64,7 @@ scroll-behavior: auto !important; } - .animate-tab-fade-in, - .snake-shake, - .snake-food-pulse, - .snake-particle { + .animate-tab-fade-in { animation: none; } }