From f27830d19ede7186bb9ad9c68dc2b80813159f6e Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 30 Jan 2026 08:22:14 -0600 Subject: [PATCH] Add frontend CLAUDE.md with coding standards - Vue 3 Composition API patterns ( + + + + +``` + +### TypeScript + +**Use strict typing:** +```typescript +// Good - explicit types +interface CardProps { + cardId: string + size?: 'sm' | 'md' | 'lg' +} + +// Good - type imports +import type { Card, GameState } from '@/types' + +// Good - const assertions for literals +const SIZES = ['sm', 'md', 'lg'] as const +type Size = typeof SIZES[number] + +// Avoid - any +const data: any = response // NO +const data: unknown = response // OK if you'll narrow it +``` + +### Naming Conventions + +| Type | Convention | Example | +|------|------------|---------| +| Components | PascalCase | `CardDisplay.vue`, `DeckBuilder.vue` | +| Composables | camelCase with `use` prefix | `useAuth.ts`, `useDeckValidation.ts` | +| Stores | camelCase with descriptive name | `auth.ts`, `game.ts` | +| Types/Interfaces | PascalCase | `Card`, `GameState`, `ApiResponse` | +| Constants | UPPER_SNAKE_CASE | `MAX_HAND_SIZE`, `API_BASE_URL` | +| CSS classes | kebab-case (BEM optional) | `card-display`, `card-display--active` | + +--- + +## Pinia Stores + +**Use setup store syntax:** +```typescript +// stores/game.ts +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { GameState } from '@/types' + +export const useGameStore = defineStore('game', () => { + // State + const gameState = ref(null) + const isConnected = ref(false) + + // Getters (computed) + const myHand = computed(() => gameState.value?.myHand ?? []) + const isMyTurn = computed(() => gameState.value?.currentPlayer === 'me') + + // Actions + function setGameState(state: GameState) { + gameState.value = state + } + + function clearGame() { + gameState.value = null + isConnected.value = false + } + + return { + // State + gameState, + isConnected, + // Getters + myHand, + isMyTurn, + // Actions + setGameState, + clearGame, + } +}) +``` + +--- + +## Composables + +**Pattern for API composables:** +```typescript +// composables/useDecks.ts +import { ref } from 'vue' +import { apiClient } from '@/api/client' +import type { Deck, DeckCreate } from '@/types' + +export function useDecks() { + const decks = ref([]) + const isLoading = ref(false) + const error = ref(null) + + async function fetchDecks() { + isLoading.value = true + error.value = null + try { + decks.value = await apiClient.get('/api/decks') + } catch (e) { + error.value = e instanceof Error ? e.message : 'Failed to fetch decks' + } finally { + isLoading.value = false + } + } + + async function createDeck(data: DeckCreate) { + isLoading.value = true + error.value = null + try { + const deck = await apiClient.post('/api/decks', data) + decks.value.push(deck) + return deck + } catch (e) { + error.value = e instanceof Error ? e.message : 'Failed to create deck' + throw e + } finally { + isLoading.value = false + } + } + + return { + decks, + isLoading, + error, + fetchDecks, + createDeck, + } +} +``` + +--- + +## Vue-Phaser Integration + +**Phaser is for rendering only. Game logic lives in backend.** + +### Communication Pattern + +```typescript +// Vue -> Phaser (intentions) +phaserGame.value?.events.emit('card:play', { cardId, targetZone }) +phaserGame.value?.events.emit('attack:select', { attackIndex }) + +// Phaser -> Vue (completions/UI requests) +phaserGame.value?.events.on('animation:complete', handleAnimationComplete) +phaserGame.value?.events.on('card:clicked', handleCardClicked) +``` + +### State Sync + +```typescript +// Phaser scene reads from Pinia store +import { useGameStore } from '@/stores/game' + +class MatchScene extends Phaser.Scene { + private gameStore = useGameStore() + + update() { + // React to store changes + if (this.gameStore.gameState) { + this.renderState(this.gameStore.gameState) + } + } +} +``` + +--- + +## Tailwind Guidelines + +**Mobile-first responsive:** +```html + +
+
+ +
+
+``` + +**Use design system colors:** +```html + +
Fire Type
+
Water Type
+ + + +``` + +--- + +## Testing + +**Component tests with Vitest + Vue Test Utils:** +```typescript +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import CardDisplay from '@/components/cards/CardDisplay.vue' + +describe('CardDisplay', () => { + it('renders card name', () => { + /** + * Test that the card component displays the card name. + * + * Card names must be visible for players to identify cards + * in their hand and on the board. + */ + const wrapper = mount(CardDisplay, { + props: { card: { id: '1', name: 'Pikachu', hp: 60 } } + }) + expect(wrapper.text()).toContain('Pikachu') + }) + + it('emits click event with card id', async () => { + /** + * Test that clicking a card emits the correct event. + * + * Card clicks drive all game interactions - playing cards, + * selecting targets, viewing details. + */ + const wrapper = mount(CardDisplay, { + props: { card: { id: '1', name: 'Pikachu', hp: 60 } } + }) + await wrapper.trigger('click') + expect(wrapper.emitted('click')?.[0]).toEqual(['1']) + }) +}) +``` + +--- + +## Critical Rules + +1. **Mobile-first** - Base styles for mobile, use `md:` and `lg:` for larger screens +2. **TypeScript strict** - No `any`, explicit types for props/emits/returns +3. **Composition API only** - No Options API, use `