Add frontend CLAUDE.md with coding standards
- Vue 3 Composition API patterns (<script setup>) - Pinia setup store syntax - Composable patterns for API calls - Vue-Phaser integration guidelines - Tailwind mobile-first approach - TypeScript strict mode requirements - Testing patterns with docstrings - Path aliases and environment config - Common patterns (loading states, guards, error handling) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
29b5d36621
commit
f27830d19e
453
frontend/CLAUDE.md
Normal file
453
frontend/CLAUDE.md
Normal file
@ -0,0 +1,453 @@
|
||||
# Mantimon TCG Frontend - AI Agent Guidelines
|
||||
|
||||
Guidelines for AI agents working on the frontend codebase.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Technology | Purpose |
|
||||
|------------|---------|
|
||||
| Vue 3 | UI framework (Composition API + `<script setup>`) |
|
||||
| Phaser 3 | Game canvas (matches, pack opening) |
|
||||
| TypeScript | Type safety (strict mode) |
|
||||
| Pinia | State management |
|
||||
| Tailwind CSS | Styling (mobile-first) |
|
||||
| Socket.io-client | Real-time communication |
|
||||
| Vite | Build tool |
|
||||
| Vitest | Unit/component testing |
|
||||
|
||||
---
|
||||
|
||||
## Quick Commands
|
||||
|
||||
```bash
|
||||
npm run dev # Start dev server
|
||||
npm run build # Production build
|
||||
npm run preview # Preview production build
|
||||
npm run test # Run tests
|
||||
npm run test:watch # Run tests in watch mode
|
||||
npm run lint # ESLint
|
||||
npm run typecheck # TypeScript check
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/ # API client and composables
|
||||
│ ├── client.ts # Base HTTP client with auth
|
||||
│ └── types.ts # API response types
|
||||
├── assets/ # Static assets, global CSS
|
||||
├── components/ # Reusable Vue components
|
||||
│ ├── ui/ # Generic UI (Button, Modal, Toast)
|
||||
│ ├── cards/ # Card display components
|
||||
│ ├── deck/ # Deck builder components
|
||||
│ └── game/ # Game UI overlays
|
||||
├── composables/ # Vue composables (useAuth, useDecks, etc.)
|
||||
├── game/ # Phaser game code
|
||||
│ ├── scenes/ # Phaser scenes
|
||||
│ ├── objects/ # Game objects (Card, Board)
|
||||
│ ├── animations/ # Animation helpers
|
||||
│ └── bridge.ts # Vue-Phaser communication
|
||||
├── layouts/ # Layout components
|
||||
├── pages/ # Route pages
|
||||
├── router/ # Vue Router config
|
||||
├── socket/ # Socket.IO client
|
||||
├── stores/ # Pinia stores
|
||||
├── types/ # TypeScript types
|
||||
└── utils/ # Utility functions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Style
|
||||
|
||||
### Vue Components
|
||||
|
||||
**Single File Component structure:**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// 1. Imports (Vue, then external, then local)
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
import { useGameStore } from '@/stores/game'
|
||||
import type { Card } from '@/types'
|
||||
|
||||
// 2. Props and emits
|
||||
const props = defineProps<{
|
||||
cardId: string
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [cardId: string]
|
||||
}>()
|
||||
|
||||
// 3. Composables and stores
|
||||
const gameStore = useGameStore()
|
||||
|
||||
// 4. Reactive state
|
||||
const isHovered = ref(false)
|
||||
|
||||
// 5. Computed
|
||||
const card = computed(() => gameStore.getCard(props.cardId))
|
||||
|
||||
// 6. Methods
|
||||
function handleClick() {
|
||||
emit('click', props.cardId)
|
||||
}
|
||||
|
||||
// 7. Lifecycle
|
||||
onMounted(() => {
|
||||
// ...
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Single root element preferred -->
|
||||
<div
|
||||
class="card"
|
||||
:class="{ 'card--hovered': isHovered }"
|
||||
@click="handleClick"
|
||||
@mouseenter="isHovered = true"
|
||||
@mouseleave="isHovered = false"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Component-specific styles if Tailwind isn't enough */
|
||||
</style>
|
||||
```
|
||||
|
||||
### 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<GameState | null>(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<Deck[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function fetchDecks() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
decks.value = await apiClient.get<Deck[]>('/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<Deck>('/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
|
||||
<!-- Base styles for mobile, override for larger screens -->
|
||||
<div class="p-2 md:p-4 lg:p-6">
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
<!-- ... -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Use design system colors:**
|
||||
```html
|
||||
<!-- Pokemon type colors defined in tailwind.config.js -->
|
||||
<div class="bg-type-fire text-white">Fire Type</div>
|
||||
<div class="bg-type-water text-white">Water Type</div>
|
||||
|
||||
<!-- UI colors -->
|
||||
<button class="bg-primary hover:bg-primary-dark">Action</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 `<script setup>`
|
||||
4. **Phaser for rendering** - No game logic in Phaser, only visualization
|
||||
5. **Backend is authoritative** - Never trust client state for game logic
|
||||
6. **Test docstrings** - Every test needs a docstring explaining what and why
|
||||
|
||||
---
|
||||
|
||||
## Path Aliases
|
||||
|
||||
```typescript
|
||||
// Use @/ for src/
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import CardDisplay from '@/components/cards/CardDisplay.vue'
|
||||
import type { Card } from '@/types'
|
||||
|
||||
// Don't use relative paths for deeply nested imports
|
||||
import { useAuth } from '../../../composables/useAuth' // NO
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# .env.development
|
||||
VITE_API_BASE_URL=http://localhost:8000
|
||||
VITE_WS_URL=http://localhost:8000
|
||||
|
||||
# .env.production
|
||||
VITE_API_BASE_URL=https://api.play.mantimon.com
|
||||
VITE_WS_URL=https://api.play.mantimon.com
|
||||
```
|
||||
|
||||
**Access in code:**
|
||||
```typescript
|
||||
const apiUrl = import.meta.env.VITE_API_BASE_URL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Loading States
|
||||
```vue
|
||||
<template>
|
||||
<div v-if="isLoading" class="flex justify-center p-8">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
<div v-else-if="error" class="text-error p-4">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<!-- Content -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Route Guards
|
||||
```typescript
|
||||
// router/guards.ts
|
||||
export function requireAuth(to, from, next) {
|
||||
const auth = useAuthStore()
|
||||
if (!auth.isAuthenticated) {
|
||||
next({ name: 'Login', query: { redirect: to.fullPath } })
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Error Handling
|
||||
```typescript
|
||||
try {
|
||||
await apiClient.post('/api/games', data)
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
if (e.status === 401) {
|
||||
// Token expired, redirect to login
|
||||
auth.logout()
|
||||
} else {
|
||||
toast.error(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- `PROJECT_PLAN_FRONTEND.json` - Phase plan and tasks
|
||||
- `../backend/CLAUDE.md` - Backend guidelines
|
||||
- `../CLAUDE.md` - Project-wide guidelines
|
||||
Loading…
Reference in New Issue
Block a user