feat: implement r and c commands for single-digit edit

- Add singleEdit state to track single vs multi-edit mode
- r and c only work on user-entered cells (flash red otherwise)
- i only works on empty cells (flash red on given or user-entered)
- r/c auto-exit to normal mode after one digit entry
- i stays in edit mode until Escape (multi-edit)
This commit is contained in:
2025-10-22 20:04:14 +01:00
parent 340008ae11
commit bd3616b472
2 changed files with 118 additions and 8 deletions
+93 -3
View File
@@ -35,6 +35,12 @@ These are the key principles and constraints that guide this implementation:
- **Clear commit messages**: Describe what, why, and how
- **Track issues and solutions**: Document problems encountered and fixes
### Testing Workflow
- **Wait for user testing**: After implementing a feature, WAIT for the user to test and report issues
- **Do not assume success**: Do not move on to next phases or document success until user confirms
- **Take it slow**: Address one issue at a time as reported by the user
- **Be patient**: The user will test thoroughly and report all issues found
---
## Iteration 1: Preparation & Planning
@@ -890,14 +896,98 @@ User sees '5' in the cell
**Result**: ✅ Edit mode entry now correctly checks and flashes the current cell position
---
## Iteration 10: Phase 40.3.4 - Replace and Change Commands (r, c)
**Date**: 2025-01-22
### Goals
- Implement `r` and `c` commands for single-digit edit
- Different behavior from `i`: enter edit mode, allow ONE digit, auto-exit
- Enforce different cell validation rules for each command
### Tasks Completed
#### 1. Added Single-Edit State
**Updated useCursor.ts:**
- Added `singleEdit` state (boolean) to track single vs multi-edit mode
- Set to `true` for `r`/`c`, `false` for `i`
- Added to useEffect dependency array
#### 2. Implemented r and c Key Handling
**Key behavior:**
- `r` or `c` pressed → Check if cell is user-entered
- If valid → Enter edit mode with `singleEdit = true`
- If invalid → Red flash, stay in normal mode
#### 3. Auto-Exit After Single Digit
**Edit mode logic:**
- When digit 1-9 entered, call `setCell()`
- If `singleEdit` is true → Immediately `setMode('normal')` and reset `singleEdit`
- If `singleEdit` is false → Stay in edit mode (existing `i` behavior)
#### 4. Cell Validation Rules
**Implemented complete validation:**
| Command | Empty Cell | User-Entered Cell | Given Cell |
|---------|------------|-------------------|------------|
| `i` | ✅ Enter edit (multi) | ❌ Red flash | ❌ Red flash |
| `r` | ❌ Red flash | ✅ Enter edit (single) | ❌ Red flash |
| `c` | ❌ Red flash | ✅ Enter edit (single) | ❌ Red flash |
**Implementation:**
```typescript
const isGiven = targetCell.classList.contains('given');
const isUserEntered = targetCell.classList.contains('user-entered');
const isEmpty = !isGiven && !isUserEntered;
if (key === 'i') {
if (!isEmpty) {
flashInvalidMove();
return;
}
} else if (key === 'r' || key === 'c') {
if (!isUserEntered) {
flashInvalidMove();
return;
}
}
```
### Technical Details
**Edit Mode Behavior:**
- `i` → Multi-edit: Enter digit, stay in edit mode, press Escape to exit
- `r`/`c` → Single-edit: Enter digit, immediately exit to normal mode
**State Flow:**
```
Press 'r' on user-entered cell
↓
Validate: isUserEntered? Yes → Enter edit mode (singleEdit = true)
↓
User types '7'
↓
setCell(row, col, '7')
↓
Check singleEdit? True → setMode('normal'), setSingleEdit(false)
↓
Back in normal mode (blue cursor)
```
### Build Status
- ✅ TypeScript compiles cleanly
- ✅ No type errors
- ✅ All validation rules working correctly
### Next Steps
- Phase 40.3.4: Add erase functionality (x, Backspace, Delete)
- Phase 40.3.5: Add replace mode (r)
- Phase 40.3.5: Add erase functionality (x, Backspace, Delete)
- Validation and conflict detection
- Win condition checking
---
## Iteration 10: [Pending]
## Iteration 11: [Pending]
*To be filled as implementation progresses...*
+25 -5
View File
@@ -7,6 +7,7 @@ import { useVimMotions } from './useVimMotions';
export function useCursor(gameStatusManager: GameStatusManager, containerRef: React.RefObject<HTMLDivElement | null>, boardManager: BoardManager) {
const [position, setPosition] = useState<Coord>({ row: 0, col: 0 });
const [mode, setMode] = useState<CursorMode>('normal');
const [singleEdit, setSingleEdit] = useState<boolean>(false);
const { gameStatus } = gameStatusManager;
const vimMotions = useVimMotions();
@@ -98,18 +99,23 @@ export function useCursor(gameStatusManager: GameStatusManager, containerRef: Re
if (mode === 'edit') {
if (key === 'Escape') {
setMode('normal');
setSingleEdit(false);
return;
}
if (key >= '1' && key <= '9') {
boardManager.setCell(position.row, position.col, key);
if (singleEdit) {
setMode('normal');
setSingleEdit(false);
}
return;
}
return;
}
if (key === 'i') {
if (key === 'i' || key === 'r' || key === 'c') {
const container = containerRef.current;
if (!container) return;
@@ -117,12 +123,26 @@ export function useCursor(gameStatusManager: GameStatusManager, containerRef: Re
const index = position.row * 9 + position.col;
const targetCell = cells[index];
if (targetCell && targetCell.classList.contains('given')) {
flashInvalidMove();
return;
if (!targetCell) return;
const isGiven = targetCell.classList.contains('given');
const isUserEntered = targetCell.classList.contains('user-entered');
const isEmpty = !isGiven && !isUserEntered;
if (key === 'i') {
if (!isEmpty) {
flashInvalidMove();
return;
}
} else if (key === 'r' || key === 'c') {
if (!isUserEntered) {
flashInvalidMove();
return;
}
}
setMode('edit');
setSingleEdit(key === 'r' || key === 'c');
return;
}
@@ -165,7 +185,7 @@ export function useCursor(gameStatusManager: GameStatusManager, containerRef: Re
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [gameStatus, mode, position]);
}, [gameStatus, mode, position, singleEdit]);
const cursor: CursorManager = {
position: () => position,