feat: implement digit entry in edit mode (Phase 40.3.3)

- Add setCell method to useBoard to update board state
- Handle digit keys 1-9 in edit mode in useCursor
- Move boardRendered counter to useBoard for proper cursor re-rendering
- Fix golden glow disappearing after digit entry
- Digits display with .user-entered class via renderer
This commit is contained in:
2025-10-22 19:49:12 +01:00
parent 6a4b72075c
commit bc8eeaf400
5 changed files with 151 additions and 9 deletions
+124 -1
View File
@@ -762,6 +762,129 @@ Red flash → Stay in Normal
---
## Iteration 9: [Pending]
## Iteration 9: Phase 40.3.3 - Digit Entry
**Date**: 2025-01-22
### Goals
- Allow user to type digits 1-9 when in edit mode
- Update board state with entered digits
- Cells show digits with `.user-entered` class
- Stay in edit mode after entering digit
### Tasks Completed
#### 1. Added setCell Method to BoardManager
**Updated useBoard.ts:**
- Added `setCell(row, col, value)` function
- Updates board string at calculated index
- Triggers re-render to display change
**Implementation:**
```typescript
function setCell(row: number, col: number, value: string): void {
const board = boardRef.current.split('');
const index = row * 9 + col;
board[index] = value;
boardRef.current = board.join('');
renderBoard();
}
```
#### 2. Updated BoardManager Type
- Added `setCell: (row: number, col: number, value: string) => void;` to type definition
#### 3. Enhanced useCursor for Digit Entry
**Updated useCursor.ts:**
- Added `boardManager` parameter (BoardManager)
- In edit mode, detect digit keys (1-9)
- Call `boardManager.setCell()` to update board
- Stay in edit mode (no auto-exit)
**Key Handling:**
```typescript
if (mode === 'edit') {
if (key === 'Escape') {
setMode('normal');
return;
}
if (key >= '1' && key <= '9') {
boardManager.setCell(position.row, position.col, key);
return;
}
return;
}
```
#### 4. Updated useGame Integration
- Pass `board` (BoardManager) to `useCursor()`
- Three-parameter call: `useCursor(gameStatusManager, containerRef, board)`
### Technical Details
**Board State Flow:**
```
User presses '5' in edit mode
↓
useCursor detects digit key
↓
Calls boardManager.setCell(row, col, '5')
↓
Updates boardRef.current string
↓
Calls renderBoard()
↓
SudokuRenderer.render() displays digit with .user-entered class
↓
User sees '5' in the cell
```
**Cell Classification:**
- Given cells: Have `.given` class (from initial puzzle)
- User-entered cells: Have `.user-entered` class (from renderer logic)
- Empty cells: No value displayed
**Mode Behavior:**
- Edit mode entered with `i` on empty/user-entered cells
- Typing 1-9 places digit, stays in edit mode
- `Escape` exits to normal mode
- Can navigate to other cells and repeat
### Build Status
- ✅ TypeScript compiles cleanly
- ✅ No type errors
- ✅ Integration complete
### Issue 1: Golden Glow Disappearing After Digit Entry
**Problem**: When typing a digit in edit mode, the `.editing` class (golden glow) disappeared even though we remained in edit mode.
**Root Cause**: `setCell()` calls `renderBoard()` which rebuilds the entire DOM, wiping out the cursor highlight classes.
**Solution**: Moved `boardRendered` counter from `useGame` to `useBoard`
- Added `boardRendered` state to `useBoard` hook
- Increment counter at end of `renderBoard()` function
- Export `boardRendered` from `useBoard`
- `useCursorRender` depends on `boardRendered` and re-applies cursor classes after DOM rebuild
**Changes**:
- `useBoard.ts`: Added `useState` for `boardRendered`, increment in `renderBoard()`
- `useBoard/types.ts`: Added `boardRendered: number` to `BoardManager`
- `useGame.ts`: Removed local `boardRendered` state, use board's `boardRendered` instead
- Removed manual `setBoardRendered` calls from `useGame` (automatic now)
**Result**: ✅ Golden glow persists when typing digits in edit mode
### Next Steps
- Phase 40.3.4: Add erase functionality (x, Backspace, Delete)
- Phase 40.3.5: Add replace mode (r)
- Validation and conflict detection
- Win condition checking
---
## Iteration 10: [Pending]
*To be filled as implementation progresses...*
+2
View File
@@ -3,6 +3,8 @@ import type { Difficulty } from '../types';
export type BoardManager = {
containerRef: RefObject<HTMLDivElement | null>;
boardRendered: number;
generateBoard: (difficulty: Difficulty) => void;
renderBoard: () => void;
setCell: (row: number, col: number, value: string) => void;
};
+14 -1
View File
@@ -1,4 +1,4 @@
import { useRef } from 'react';
import { useRef, useState } from 'react';
import { SudokuGenerator } from './SudokuGenerator';
import { SudokuRenderer } from './SudokuRenderer';
import type { Difficulty } from './SudokuGenerator';
@@ -10,6 +10,7 @@ export function useBoard() {
const boardRef = useRef<string>('');
const solutionRef = useRef<string>('');
const initialBoardRef = useRef<string>('');
const [boardRendered, setBoardRendered] = useState(0);
function generateBoard(difficulty: Difficulty = 'easy') {
if (!generatorRef.current) {
@@ -41,6 +42,16 @@ export function useBoard() {
} else {
rendererRef.current.renderEmpty();
}
setBoardRendered(prev => prev + 1);
}
function setCell(row: number, col: number, value: string): void {
const board = boardRef.current.split('');
const index = row * 9 + col;
board[index] = value;
boardRef.current = board.join('');
renderBoard();
}
return {
@@ -50,7 +61,9 @@ export function useBoard() {
boardRef,
solutionRef,
initialBoardRef,
boardRendered,
generateBoard,
renderBoard,
setCell,
};
}
+9 -1
View File
@@ -1,9 +1,10 @@
import { useState, useEffect } from 'react';
import type { Coord, CursorManager, CursorMode } from './types';
import type { GameStatusManager } from '../useGameStatus/types';
import type { BoardManager } from '../useBoard/types';
import { useVimMotions } from './useVimMotions';
export function useCursor(gameStatusManager: GameStatusManager, containerRef: React.RefObject<HTMLDivElement | null>) {
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 { gameStatus } = gameStatusManager;
@@ -97,7 +98,14 @@ export function useCursor(gameStatusManager: GameStatusManager, containerRef: Re
if (mode === 'edit') {
if (key === 'Escape') {
setMode('normal');
return;
}
if (key >= '1' && key <= '9') {
boardManager.setCell(position.row, position.col, key);
return;
}
return;
}
+2 -6
View File
@@ -8,15 +8,14 @@ import { useCursorRender } from './useCursor/useCursorRender';
export function useGame(options: GameOptions = {}, platformHook?: unknown): GameManager {
const { difficulty = 'easy' } = options;
const board = useBoard();
const { containerRef, renderBoard, generateBoard } = board;
const { containerRef, renderBoard, generateBoard, boardRendered } = board;
const gameStatusManager = useGameStatus();
const { gameStatus } = gameStatusManager;
const [keyLog, setKeyLog] = useState<{ key: string; timestamp: number }[]>([]);
const [boardRendered, setBoardRendered] = useState(0);
const cursor = useCursor(gameStatusManager, containerRef);
const cursor = useCursor(gameStatusManager, containerRef, board);
const cursorPosition = cursor.position();
const cursorMode = cursor.mode();
@@ -24,17 +23,14 @@ export function useGame(options: GameOptions = {}, platformHook?: unknown): Game
useEffect(() => {
renderBoard();
setBoardRendered(prev => prev + 1);
}, []);
useEffect(() => {
if (gameStatus === 'started') {
generateBoard(difficulty);
renderBoard();
setBoardRendered(prev => prev + 1);
} else if (gameStatus === 'waiting') {
renderBoard();
setBoardRendered(prev => prev + 1);
}
}, [gameStatus, difficulty]);