docs: add database migration guide and playing guide modal spec

- Document proper process for creating database migrations with drizzle-kit
- Add warnings about NOT manually editing migration files
- Document Rithmomachia playing guide modal specifications

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-11-01 14:46:56 -05:00
parent 4d4d930bd3
commit 5a29af78e2
2 changed files with 443 additions and 0 deletions

View File

@@ -342,6 +342,59 @@ This project uses SQLite with Drizzle ORM. Database location: `./data/sqlite.db`
- Drizzle config: `drizzle.config.ts`
- Migrations: `drizzle/` directory
### Creating Database Migrations
**CRITICAL: NEVER manually create migration SQL files or edit the journal.**
When adding/modifying database schema:
1. **Update the schema file** in `src/db/schema/`:
```typescript
// Example: Add new column to existing table
export const abacusSettings = sqliteTable('abacus_settings', {
userId: text('user_id').primaryKey(),
// ... existing columns ...
newField: integer('new_field', { mode: 'boolean' }).notNull().default(false),
})
```
2. **Generate migration using drizzle-kit**:
```bash
npx drizzle-kit generate --custom
```
This creates:
- A new SQL file in `drizzle/####_name.sql`
- Updates `drizzle/meta/_journal.json`
- Creates a snapshot in `drizzle/meta/####_snapshot.json`
3. **Edit the generated SQL file** (it will be empty):
```sql
-- Custom SQL migration file, put your code below! --
ALTER TABLE `abacus_settings` ADD `new_field` integer DEFAULT 0 NOT NULL;
```
4. **Test the migration** on your local database:
```bash
npm run db:migrate
```
5. **Verify** the column was added:
```bash
mcp__sqlite__describe_table table_name
```
**What NOT to do:**
- ❌ DO NOT manually create SQL files in `drizzle/` without using `drizzle-kit generate`
- ❌ DO NOT manually edit `drizzle/meta/_journal.json`
- ❌ DO NOT run SQL directly with `sqlite3` command
- ❌ DO NOT use `drizzle-kit generate` without `--custom` flag (it requires interactive prompts)
**Why this matters:**
- Drizzle tracks applied migrations in `__drizzle_migrations` table
- Manual SQL files won't be tracked properly
- Production deployments run `npm run db:migrate` automatically
- Improperly created migrations will fail in production
## Deployment Verification
**CRITICAL: Never assume deployment is complete just because the website is accessible.**

View File

@@ -0,0 +1,390 @@
# PlayingGuideModal - Complete Feature Specification
## Overview
Interactive, draggable, resizable modal for Rithmomachia game guide with i18n support and bust-out functionality.
## File Location
`src/arcade-games/rithmomachia/components/PlayingGuideModal.tsx`
## Dependencies
```typescript
import { useEffect, useState, useRef } from 'react'
import * as Dialog from '@radix-ui/react-dialog'
import { useTranslation } from 'react-i18next'
import { css } from '../../../../styled-system/css'
import { Z_INDEX } from '@/constants/zIndex'
import { useAbacusSettings } from '@/hooks/useAbacusSettings'
import { PieceRenderer } from './PieceRenderer'
import { RithmomachiaBoard, type ExamplePiece } from './RithmomachiaBoard'
import type { PieceType, Color } from '../types'
import '../i18n/config' // Initialize i18n
```
## Props Interface
```typescript
interface PlayingGuideModalProps {
isOpen: boolean // Controls visibility
onClose: () => void // Called when modal closes
standalone?: boolean // True when opened in popup window (full-screen mode)
}
```
## State Management
### Required State
```typescript
const { t, i18n } = useTranslation()
const { data: abacusSettings } = useAbacusSettings()
const useNativeAbacusNumbers = abacusSettings?.nativeAbacusNumbers ?? false
const [activeSection, setActiveSection] = useState<Section>('overview')
const [position, setPosition] = useState({ x: 0, y: 0 })
const [size, setSize] = useState({ width: 450, height: 600 })
const [isDragging, setIsDragging] = useState(false)
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
const [isResizing, setIsResizing] = useState(false)
const [resizeDirection, setResizeDirection] = useState<string>('')
const [isHovered, setIsHovered] = useState(false)
const modalRef = useRef<HTMLDivElement>(null)
```
### Section Type
```typescript
type Section = 'overview' | 'pieces' | 'capture' | 'strategy' | 'harmony' | 'victory'
```
## Core Features
### 1. Radix Dialog Wrapper
**When NOT standalone:**
- Wrap entire modal in `<Dialog.Root open={isOpen} onOpenChange={onClose}>`
- Use `<Dialog.Portal>` for portal rendering
- Use `<Dialog.Overlay>` with backdrop styling
- Use `<Dialog.Content>` as container for draggable/resizable content
**Styling:**
- Overlay: semi-transparent black (`rgba(0, 0, 0, 0.5)`)
- Content: no default positioning (we control via position state)
- Z-index: Must be above game board - use `Z_INDEX.GAME.GUIDE_MODAL` or 15000+
**When standalone:**
- Skip Dialog wrapper entirely
- Render full-screen fixed container
### 2. Draggable Functionality
**Requirements:**
- Click and drag from header to move modal
- Disabled on mobile (`window.innerWidth < 768`)
- Cursor changes to 'move' when hovering header
- Position state tracks x, y coordinates
**Implementation:**
```typescript
const handleMouseDown = (e: React.MouseEvent) => {
if (window.innerWidth < 768) return
setIsDragging(true)
setDragStart({
x: e.clientX - position.x,
y: e.clientY - position.y,
})
}
```
**Effects:**
- Global `mousemove` listener updates position while dragging
- Global `mouseup` listener stops dragging
- Cleanup listeners on unmount
### 3. Resizable Functionality
**Requirements:**
- 8 resize handles: N, S, E, W, NE, NW, SE, SW
- Handles visible only on hover (when `isHovered === true`)
- Disabled on mobile
- Min size: 450x600
- Max size: 90vw x 80vh
**Handle Positions & Cursors:**
- N (top): `cursor: 'ns-resize'`
- S (bottom): `cursor: 'ns-resize'`
- E (right): `cursor: 'ew-resize'`
- W (left): `cursor: 'ew-resize'`
- NE (top-right): `cursor: 'nesw-resize'`
- NW (top-left): `cursor: 'nwse-resize'`
- SE (bottom-right): `cursor: 'nwse-resize'`
- SW (bottom-left): `cursor: 'nesw-resize'`
**Handle Styling:**
- Width/height: 8px (invisible hit area)
- Visible border when hovered: 2px solid blue
- Positioned absolutely at edges/corners
**Implementation:**
```typescript
const handleResizeStart = (e: React.MouseEvent, direction: string) => {
if (window.innerWidth < 768) return
e.stopPropagation()
setIsResizing(true)
setResizeDirection(direction)
setDragStart({ x: e.clientX, y: e.clientY })
}
```
### 4. Bust-Out Button
**Location:** Header, right side (before close button)
**Icon:** ↗ or external link icon
**Functionality:**
```typescript
const handleBustOut = () => {
const url = window.location.origin + '/arcade/rithmomachia/guide'
const features = 'width=600,height=800,menubar=no,toolbar=no,location=no,status=no'
window.open(url, 'RithmomachiaGuide', features)
}
```
**Visibility:** Only show if NOT already standalone
**Route:** Must have a route at `/arcade/rithmomachia/guide` that renders:
```tsx
<PlayingGuideModal isOpen={true} onClose={() => window.close()} standalone={true} />
```
### 5. Internationalization
**Setup:**
- i18n config file: `src/arcade-games/rithmomachia/i18n/config.ts`
- Translation files in: `src/arcade-games/rithmomachia/i18n/locales/`
- Languages: en.json, de.json (minimum)
**Usage:**
- All text uses `t('guide.section.key')` format
- Language switcher in header with buttons for each language
**Header Language Switcher:**
```tsx
<div className={css({ display: 'flex', gap: '8px' })}>
{['en', 'de'].map((lang) => (
<button
key={lang}
onClick={() => i18n.changeLanguage(lang)}
className={css({
px: '8px',
py: '4px',
fontSize: '12px',
fontWeight: i18n.language === lang ? 'bold' : 'normal',
bg: i18n.language === lang ? '#3b82f6' : '#e5e7eb',
color: i18n.language === lang ? 'white' : '#374151',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
})}
>
{lang.toUpperCase()}
</button>
))}
</div>
```
### 6. Centering on Mount
**Effect:**
```typescript
useEffect(() => {
if (isOpen && modalRef.current && !standalone) {
const rect = modalRef.current.getBoundingClientRect()
setPosition({
x: (window.innerWidth - rect.width) / 2,
y: Math.max(50, (window.innerHeight - rect.height) / 2),
})
}
}, [isOpen, standalone])
```
**Standalone Mode:**
- If standalone, don't center - use full viewport
- Position: fixed, top: 0, left: 0, width: 100vw, height: 100vh
## Layout Structure
```
<Dialog.Root> (if not standalone)
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content asChild>
<div ref={modalRef} style={{ position: absolute, top: position.y, left: position.x }}>
{/* Resize handles (8 total, only if hovered and not mobile) */}
<div> {/* Main container */}
{/* Header */}
<div onMouseDown={handleMouseDown} style={{ cursor: isDragging ? 'grabbing' : 'grab' }}>
<h2>{t('guide.title')}</h2>
<div> {/* Language switcher */}
<button onClick={handleBustOut}> {/* Bust-out (if not standalone) */}
<button onClick={onClose}> {/* Close X */}
</div>
{/* Navigation tabs */}
<div> {/* Section buttons: Overview, Pieces, Capture, Strategy, Harmony, Victory */}
{/* Content area - scrollable */}
<div style={{ overflow: 'auto', maxHeight: size.height - headerHeight }}>
{activeSection === 'overview' && <OverviewSection />}
{activeSection === 'pieces' && <PiecesSection useNativeAbacusNumbers={useNativeAbacusNumbers} />}
{activeSection === 'capture' && <CaptureSection />}
{/* ... etc */}
</div>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
```
## Styling Requirements
### Main Container
- Background: `#ffffff`
- Border radius: `12px`
- Box shadow: `0 20px 60px rgba(0, 0, 0, 0.3)`
- Border: `1px solid #e5e7eb`
- Position: `absolute` (controlled by position state)
- Width/height: from size state
### Header
- Background: `#f9fafb`
- Border bottom: `1px solid #e5e7eb`
- Padding: `16px`
- Display: flex, justify-between, align-items: center
- Cursor: `move` on desktop (when not standalone)
- Prevent text selection while dragging
### Navigation Tabs
- Display: flex, gap: `8px`
- Padding: `12px 16px`
- Background: `#ffffff`
- Border bottom: `1px solid #e5e7eb`
### Tab Buttons
- Active: bold, blue background, white text
- Inactive: normal weight, gray background, dark text
- Padding: `8px 16px`
- Border radius: `6px`
- Cursor: pointer
- Transition: all 0.2s
### Content Area
- Padding: `24px`
- Overflow: auto
- Max height: calculated (size.height - header - tabs)
- Color: `#374151`
- Line height: `1.6`
### Resize Handles
- Position: absolute
- Width/height: 8px
- Background: transparent
- Border: visible on hover (2px solid `#3b82f6`)
- Z-index: 1 (above content)
## Content Sections
### PiecesSection Component
**Must have its own useAbacusSettings hook:**
```typescript
function PiecesSection() {
const { data: abacusSettings } = useAbacusSettings()
const useNativeAbacusNumbers = abacusSettings?.nativeAbacusNumbers ?? false
// ... piece rendering with useNativeAbacusNumbers prop
}
```
### All RithmomachiaBoard Uses
- Must pass `useNativeAbacusNumbers={useNativeAbacusNumbers}` prop
- Boards show game positions with pieces
### All PieceRenderer Uses
- Must pass `useNativeAbacusNumbers={useNativeAbacusNumbers}` prop
- Renders individual piece icons in pieces section
## Translation Keys (Minimum Required)
```json
{
"guide": {
"title": "Rithmomachia Playing Guide",
"overview": {
"title": "Overview",
"content": "..."
},
"pieces": {
"title": "Your Pieces",
"circle": "Circle",
"triangle": "Triangle",
"square": "Square",
"pyramid": "Pyramid"
},
"capture": {
"title": "Capture Rules",
"equality": "Equality",
"multiple": "Multiple",
"ratio": "Ratio",
"sum": "Sum",
"difference": "Difference",
"product": "Product"
},
"strategy": {
"title": "Strategy Tips"
},
"harmony": {
"title": "Harmony (Progressions)"
},
"victory": {
"title": "Victory Conditions"
}
}
}
```
## Error Prevention
1. **Z-Index Issue:** Must be higher than game board (use `Z_INDEX.GAME.GUIDE_MODAL` or 15000+)
2. **Lost Work:** Never use `git checkout --` on working files without confirming stash/commit first
3. **Dialog Overlay:** Must render with high z-index to cover game
4. **Mobile:** Disable drag/resize on mobile, make responsive
5. **Standalone Route:** Must exist at `/arcade/rithmomachia/guide`
## Testing Checklist
- [ ] Modal opens and closes correctly
- [ ] Dragging works on desktop
- [ ] Resizing works on desktop (all 8 handles)
- [ ] Drag/resize disabled on mobile
- [ ] Language switcher changes content
- [ ] Bust-out button opens new window
- [ ] New window renders standalone mode correctly
- [ ] Modal appears above game board
- [ ] Close button works
- [ ] All sections render correctly
- [ ] Native abacus numbers toggle respected
- [ ] Translations load for all languages
- [ ] Modal centers on first open
- [ ] Position/size persists while open
- [ ] Cleanup happens on unmount
## Implementation Priority
1. Basic Dialog structure with standalone mode
2. Header with title, close, bust-out
3. Language switcher and i18n setup
4. Navigation tabs
5. Content sections (start with existing content)
6. Dragging functionality
7. Resizing functionality
8. Native abacus numbers integration
9. Translation files
10. Standalone route page