Frontend Architecture
Component Architecture (Atomic Design)
The frontend follows Atomic Design principles:
components/├── atoms/ # Smallest UI building blocks│ ├── Button.svelte # 5 variants, 3 sizes│ ├── Input.svelte # With validation│ ├── Badge.svelte # Tag element│ ├── ProgressBar.svelte # Progress indicator│ ├── Icon.svelte # Icon wrapper│ └── LanguageToggle.svelte # EN/DE switcher│├── molecules/ # Combinations of atoms│ ├── PromptDisplay.svelte # Shows prompt (prefix + subject + suffix)│ ├── StatItem.svelte # Player statistics│ ├── PasswordInput.svelte # Password with validation│ └── UsernameEditor.svelte # Name editor (max 32 chars)│├── organisms/ # Complex UI sections│ ├── Modal.svelte # Modal with overlay│ ├── Card.svelte # Card component│ ├── GalleryGrid.svelte # Pixel art gallery (rankings)│ ├── PlayerList.svelte # Active players + spectators│ └── CookieNotice.svelte # Privacy notice│├── features/ # Game-phase components│ ├── Landing.svelte # Interactive landing page│ ├── Lobby/│ │ ├── index.svelte # Main lobby│ │ ├── LobbyMenu.svelte # Public/Private selection│ │ └── LobbyRoom.svelte # Room view with players│ ├── Drawing.svelte # Drawing phase│ ├── Voting.svelte # Voting phase (uses Voting/)│ ├── Voting/ # Voting sub-components│ │ ├── VotingContenderCard.svelte # Contender display│ │ └── VSBadge.svelte # VS indicator│ ├── Finale.svelte # Top 10% finale│ ├── Results.svelte # Results (uses Results/)│ ├── Results/ # Results sub-components│ │ ├── ResultsPodium.svelte # Top 3 podium│ │ ├── PodiumSlot.svelte # Individual podium slot│ │ ├── ResultsGallery.svelte # Full gallery view│ │ └── GalleryItemCard.svelte # Gallery item│ ├── Countdown.svelte # Pre-game countdown│ ├── CopyCat/ # 1v1 Memory Mode│ │ ├── Memorize.svelte # Reference image display│ │ ├── CopyCatResult.svelte # Accuracy comparison│ │ └── CopyCatRematch.svelte # Rematch voting│ ├── CopyCatRoyale/ # Battle Royale Mode (NEW)│ │ ├── index.svelte # Container (routes phases)│ │ ├── RoyaleInitialDrawing.svelte # Create original art│ │ ├── RoyaleShowReference.svelte # Memorize phase│ │ ├── RoyaleDrawing.svelte # Recreate from memory│ │ ├── RoyaleResults.svelte # Round results│ │ └── RoyaleWinner.svelte # Winner announcement│ ├── PixelGuesser/ # Pictionary Mode│ │ ├── Guessing.svelte # Live drawing + guessing│ │ ├── Reveal.svelte # Answer reveal│ │ └── FinalResults.svelte # Game rankings│ ├── PixelSurvivor/ # Roguelike Mode│ │ ├── Menu.svelte # New run/continue/stats│ │ ├── Gameplay.svelte # Main gameplay loop│ │ ├── Combat/ # Combat sub-system│ │ │ ├── index.svelte # Combat container│ │ │ ├── CombatArena.svelte # Battle view│ │ │ ├── CombatActions.svelte # Action buttons│ │ │ └── CombatResult.svelte # Battle outcome│ │ └── ... (10+ components)│ └── ZombiePixel/ # Real-time Infection Mode (NEW)│ ├── index.svelte # Game container│ ├── ZombieGrid.svelte # Canvas-based 32x32 grid│ ├── ZombieGridLegend.svelte # Legend display│ ├── ZombieControls.svelte # Touch/keyboard controls│ ├── ZombieHUD.svelte # Status display│ ├── ZombieResults.svelte # End-game results│ ├── zombieGridConstants.ts # Rendering config│ └── zombieGridRenderers.ts # Canvas helpers│├── utility/ # Functional components│ ├── PixelCanvas.svelte # 8x8 drawing canvas│ ├── ColorPalette.svelte # 16-color selector│ ├── Timer.svelte # Countdown timer│ └── DemoCanvas.svelte # Landing page demo│└── debug/ # Development tools └── DebugPanel.svelteComponent Guidelines
DO
- Use design tokens (
var(--space-4)) - Keep atoms single-purpose, no business logic
- Compose molecules from atoms
- Use barrel exports for imports
- Use TypeScript interfaces for props
DON’T
- Put business logic in atoms/molecules
- Import atoms directly in features (use organisms/molecules)
- Hardcode colors or spacing values
- Create new atoms if existing ones can be extended
Svelte Stores
Connection State
connectionStatus: 'disconnected' | 'connecting' | 'connected'socketId: string | nullglobalOnlineCount: numbersessionBlocked: boolean // Too many browser sessionsidleWarning: { show: boolean; timeLeft: number }User State
interface User { displayName: string; discriminator: string; // 4 digits "0000"-"9999" fullName: string; // "Name#0000"}
currentUser: writable<User | null>Lobby State
interface LobbyState { instanceId: string | null; type: 'public' | 'private' | null; code: string | null; // 4-char room code isHost: boolean; hasPassword: boolean; players: User[]; isSpectator: boolean; onlineCount: number; gameMode: string;}Game State
type GamePhase = // Standard phases | 'idle' | 'lobby' | 'countdown' | 'drawing' | 'voting' | 'finale' | 'results' // CopyCat phases | 'memorize' | 'copycat-result' | 'copycat-rematch' // PixelGuesser phases | 'guessing' | 'reveal' | 'pixelguesser-results' // ZombiePixel phases | 'active' // CopyCatRoyale phases | 'royale-initial-drawing' | 'royale-show-reference' | 'royale-drawing' | 'royale-results' | 'royale-winner';
interface GameState { phase: GamePhase; prompt: Prompt | null; promptIndices: PromptIndices | null; timer: { duration: number; endsAt: number; remaining: number } | null;}Derived Stores
localizedPrompt // Auto-updates on language changelocalizedResultsPromptisInGame: boolean // instanceId !== nullisHost: booleanplayerCount: numberDesign Tokens
All styling uses CSS variables from lib/styles/tokens.css:
Colors (Dark Mode)
/* Backgrounds */--color-bg-primary: #0f0f23;--color-bg-secondary: #1a1a3e;--color-bg-tertiary: #252552;--color-bg-elevated: #2d2d5a;
/* Brand (Orange/Yellow) */--color-brand: #f5a623;--color-brand-light: #ffc857;
/* Accent (Cyan) */--color-accent: #4ecdc4;
/* Buttons */--color-btn-primary: #22c55e; /* Green */--color-btn-secondary: #4b5563; /* Gray */--color-btn-action: #3b82f6; /* Blue */--color-btn-danger: #dc2626; /* Red */
/* Semantic */--color-success: #22c55e;--color-warning: #f59e0b;--color-error: #ef4444;Spacing (4px base)
--space-1: 4px; --space-2: 8px;--space-3: 12px; --space-4: 16px;--space-5: 20px; --space-6: 24px;--space-8: 32px; --space-10: 40px;Typography
--font-family: 'Pixelify Sans', 'Press Start 2P', monospace;
--font-size-xs: 0.75rem; /* 12px */--font-size-sm: 0.875rem; /* 14px */--font-size-md: 1rem; /* 16px */--font-size-lg: 1.25rem; /* 20px */--font-size-xl: 1.5rem; /* 24px */Pixel-Art Rendering
.pixel-art, canvas { image-rendering: pixelated; image-rendering: crisp-edges;}Barrel Exports
// DO: Use barrel exportsimport { Button, Input } from '$lib/components/atoms';import { Modal, Card } from '$lib/components/organisms';import { Lobby, Drawing } from '$lib/components/features';
// DON'T: Direct file importsimport Button from '$lib/components/atoms/Button.svelte';Socket Bridge
The socketBridge.ts connects Socket.io events to Svelte stores:
function setupEventHandlers(socket: AppSocket) { socket.on('connected', (data) => { currentUser.set(data.user); saveUser(data.user); });
socket.on('lobby-joined', (data) => { lobby.set({...}); if (data.timerEndsAt) startTimer(...); });
socket.on('phase-changed', ({ phase, promptIndices }) => { game.update(g => ({ ...g, phase, promptIndices })); });}Internationalization (i18n)
All user-facing text uses the i18n system:
<script lang="ts"> import { t } from '$lib/i18n';</script>
<button>{$t.common.submit}</button><p>{$t.errors.roomNotFound}</p>Localized Prompts
Prompts use indices for localization:
// Server sends indices{ prefixIdx: 5, subjectIdx: 20, suffixIdx: null }
// Client localizes based on languagelocalizePrompt(indices) // → { prefix: "blue", subject: "pizza", suffix: "" } // or { prefix: "blau", subject: "Pizza", suffix: "" }