Files
vim-sudoku/IMPLEMENTATION.md
T
pastilhas bc8eeaf400 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
2025-10-22 19:49:12 +01:00

26 KiB

vim-sudoku Unified API Implementation Log

This document tracks the step-by-step implementation of the unified API architecture for vim-sudoku.

Overview

We are porting vim-sudoku from the old architecture (src_old) to follow the unified API patterns established by vim-maze. This enables consistent architecture across all VIMazing games and better platform integration.


Agent Rules

These are the key principles and constraints that guide this implementation:

React 19 Optimization

  • No useCallback or useMemo: React 19's compiler automatically optimizes, making these hooks unnecessary
  • Use plain functions instead of wrapped callbacks
  • React will handle memoization automatically
  • Cleaner, more readable code

Code Conventions

  • No comments: Code should be self-documenting unless explicitly requested
  • Type-first approach: Define types before implementation
  • Explicit imports: Always import types with import type
  • Return type annotations: Use ReturnType<typeof hookName> for hook return types

Architecture Patterns

  • Hook composition: Small, focused hooks composed together
  • Ref-based state: Use refs for non-reactive state, useState for reactive
  • Module organization: Each hook module has types.ts, implementation, and index.ts
  • Follow vim-maze patterns: Maintain consistency across games

Documentation

  • Update IMPLEMENTATION.md: After every iteration
  • Clear commit messages: Describe what, why, and how
  • Track issues and solutions: Document problems encountered and fixes

Iteration 1: Preparation & Planning

Date: 2025-01-22

Goals

  • Review existing implementations (vim-maze, src_old, src_failed)
  • Create implementation plan
  • Prepare example app for new API

Tasks Completed

1. Reviewed Architecture

  • Analyzed vim-maze unified API implementation
  • Examined src_old working implementation
  • Identified src_failed attempt issues
  • Documented key differences between vim-maze and vim-sudoku

2. Created Implementation Plan

  • Created IMPLEMENTATION_PLAN.md with 7 phases (numbered x10 for flexibility)
  • Defined file structure matching vim-maze patterns
  • Documented key design decisions
  • Established type mappings between vim-maze and vim-sudoku
  • Listed success criteria

3. Prepared Example App (Phase 5)

  • Updated example/src/App.tsx to match vim-maze structure
  • Simplified to desktop-only (no mobile support)
  • Added score metrics display (time, keystrokes, final score)
  • Updated game status messages for sudoku context
  • Changed navigation instructions (hjkl + i for edit mode)
  • Verified useKeyBindings already matches vim-maze

File Changes

vim-sudoku/
├── IMPLEMENTATION_PLAN.md       [CREATED] - Detailed implementation roadmap
├── IMPLEMENTATION.md             [CREATED] - This file, tracking progress
├── example/src/App.tsx           [MODIFIED] - Simplified, sudoku-focused UI
└── src/ → src_failed/            [RENAMED] - Previous failed attempt archived

Key Decisions

  1. No Mobile Support: Sudoku with vim bindings requires keyboard, mobile not practical
  2. Simple Metrics: Start with time, keystrokes, and final score (no efficiency yet)
  3. Phase Numbering: Use x10 numbering (10, 20, 30...) to allow insertions

Next Steps

  • Phase 10: Setup & Core Types
    • Create clean src/ directory structure
    • Define all type definitions
    • Set up module exports

Iteration 2: Phase 10 - Minimal Skeleton

Date: 2025-01-22

Goals

  • Create clean src/ directory structure
  • Implement minimal types
  • Build empty 9x9 grid rendering
  • Mock GameManager API for example app
  • Get visual feedback without errors

Tasks Completed

1. Created Core Structure

src/
├── game.css              [COPIED] - From src_old
├── index.ts              [CREATED] - Main exports
├── types.ts              [CREATED] - Basic type definitions
├── useGame.ts            [CREATED] - Orchestrator with mocks
└── useBoard/
    └── useBoard.ts       [CREATED] - Empty grid rendering

2. Implemented Types (src/types.ts)

  • Difficulty - Puzzle difficulty levels
  • GameStatus - Game state machine states
  • GameOptions - useGame configuration
  • GameManager - Full API interface (mocked for now)
  • KeyLogEntry - Keystroke tracking

3. Implemented useBoard (src/useBoard/useBoard.ts)

  • containerRef - DOM reference
  • renderBoard() - Renders empty 9x9 sudoku grid
  • Creates proper HTML structure with sudoku-grid and sudoku-cell classes
  • Sets data attributes (data-r, data-c) for cell identification

4. Implemented useGame (src/useGame.ts)

  • Calls useBoard() for grid rendering
  • Returns full GameManager interface
  • Real implementations:
    • gameStatus state management
    • startGame, togglePause, quitGame
    • keyLog tracking with clearKeyLog, getKeyLog
  • Mocked implementations:
    • cursor (empty object)
    • scoreManager (zeros and null)
  • Supports optional platformHook parameter

5. Verified Build

  • ✅ TypeScript compilation successful
  • ✅ Example app builds without errors
  • ✅ No runtime errors expected

Technical Details

Empty Grid Rendering:

  • 81 cells (9x9) created with sudoku-cell class
  • Each cell has data-r and data-c attributes
  • Wrapped in sudoku-grid div inside #sudoku container
  • Uses CSS from src_old for styling

Mocking Strategy:

  • Return full GameManager interface to satisfy example app
  • Mock methods do nothing (empty functions)
  • Mock values return defaults (0, null, empty arrays)
  • Real state management only for critical path (gameStatus)

Next Steps

  • Phase 20: Implement full useBoard with SudokuGenerator
    • Port puzzle generation logic
    • Add board state management
    • Implement cell validation

Iteration 3: Phase 20 - Puzzle Generation & Rendering

Date: 2025-01-22

Goals

  • Port SudokuGenerator from src_old
  • Generate valid sudoku puzzles
  • Render given numbers in correct positions
  • Test startGame/quitGame integration via platformHook

Tasks Completed

1. Ported SudokuGenerator

  • Copied src_old/lib/Sudoku.ts → src/useBoard/SudokuGenerator.ts
  • Full puzzle generation algorithm from robatron/sudoku.js
  • Supports all difficulty levels (easy, medium, hard, very-hard, insane, inhuman)
  • Returns 81-character string representation

2. Enhanced useBoard

  • Added boardRef - Current board state (81-char string)
  • Added initialBoardRef - Initial puzzle state for reset
  • Added generateBoard(difficulty) - Creates new puzzle
  • Updated renderBoard() - Displays numbers from board state
  • Cells show given digits, empty for '.' characters

3. Updated useGame

  • Reads difficulty from GameOptions (defaults to 'easy')
  • Calls generateBoard() when gameStatus becomes 'started'
  • Re-renders board on game status changes
  • startGame → generates puzzle and displays it
  • quitGame → clears to empty board

4. Game Flow Integration

  • Waiting state: Empty 9x9 grid displayed
  • Press Space: Triggers startGame via platformHook
  • Started state: New puzzle generated with ~62 givens (easy mode)
  • Press Q: Triggers quitGame, returns to empty grid

Technical Details

Board Representation:

boardRef.current = "52...8.4....7...4......6.....8.....3.9....8..1...2.......2....6...3....7..8...9."
// 81 characters: '1'-'9' for givens, '.' for empty cells

Rendering Logic:

  • Iterate through 81 characters
  • Create cell with data-r and data-c attributes
  • If character is digit → display in cell
  • If character is '.' → leave cell empty
  • Uses existing CSS classes from game.css

Game Status Flow:

waiting (empty grid) 
   ↓ [space pressed]
started (puzzle generated & rendered)
   ↓ [q pressed]
waiting (empty grid)

Testing Notes

  • ✅ TypeScript compiles cleanly
  • ✅ Example app builds successfully
  • ✅ Easy difficulty generates ~62 given numbers
  • ✅ platformHook integration working (startGame/quitGame)
  • Ready for browser testing with actual puzzle display

Next Steps

  • Phase 30: Implement useGameStatus hook
    • Proper state machine with transitions
    • Handle paused state
    • Integrate with scoring

Iteration 4: Phase 21 - Better Generation Algorithm

Date: 2025-01-22

Goals

  • Replace Robatron's algorithm with pattern-based generation
  • Separate generator and renderer into clean classes
  • Always generate valid, solvable puzzles
  • Store both puzzle and solution references
  • Port unit tests for validation

Tasks Completed

1. Replaced SudokuGenerator

  • Old: Robatron's backtracking algorithm (372 lines, complex)
  • New: Pattern-based generation from src_old/sudoku/ (~130 lines, simple)
  • Algorithm:
    1. Generate fully solved board using base pattern + shuffles
    2. Validate complete board (sum checks for rows/cols/boxes)
    3. Remove X cells based on difficulty to create puzzle
    4. Keep solution reference (no solving needed!)

2. Implemented Class-Based Architecture

SudokuGenerator class:

  • generate(difficulty) - Creates puzzle + solution
  • getPuzzle() - Returns puzzle string (with '.' for empty)
  • getSolution() - Returns complete solution
  • validateFullBoard() - Private validation (defensive)
  • Configurable difficulty at top of file

SudokuRenderer class:

  • render(board, initialBoard) - Renders current state
  • renderEmpty() - Renders empty 9x9 grid
  • Distinguishes given cells from user-entered cells
  • Adds CSS classes: 'given', 'user-entered'

3. Updated useBoard

  • Stores refs: generatorRef, rendererRef
  • Stores state: boardRef, solutionRef, initialBoardRef
  • Uses class instances for generation and rendering
  • Clean separation of concerns

4. Difficulty Configuration

const CELLS_TO_REMOVE = {
  easy: 40,    // ~41 givens
  medium: 50,  // ~31 givens
  hard: 60,    // ~21 givens
} as const;

5. Ported Unit Tests

  • SudokuGenerator.test.ts using bun:test
  • Tests: valid board generation (100x)
  • Tests: correct cell removal counts
  • Tests: puzzle/solution consistency

Technical Details

Why This Algorithm is Better:

  1. Always valid: Starts with mathematically valid complete board
  2. Always solvable: We keep the solution, don't need to solve
  3. Fast: No backtracking, just pattern + shuffle
  4. Simple: Easy to understand and maintain
  5. Predictable difficulty: Just cell count, not algorithmic complexity

Pattern-Based Generation:

// Base pattern ensures valid structure
pattern(r, c) = (3 * (r % 3) + floor(r / 3) + c) % 9 + 1

// Then shuffle:
// - Rows within bands (0-2, 3-5, 6-8)
// - Cols within stacks (0-2, 3-5, 6-8)
// - Band order
// - Stack order
// - Digit mapping (1-9)

Rendering Enhancement:

  • Given cells: <div class="sudoku-cell given">5</div>
  • User-entered: <div class="sudoku-cell user-entered">7</div>
  • Empty: <div class="sudoku-cell"></div>

Build Status

  • ✅ TypeScript compiles cleanly
  • ✅ Example app builds successfully
  • ✅ Unit tests ready (bun test)
  • ✅ Algorithm validated with 100+ board generations

Next Steps

  • Phase 30: Implement useGameStatus hook
    • Proper state machine
    • Handle paused state
    • Win condition detection

Iteration 5: Phase 30 - useGameStatus Hook

Date: 2025-01-22

Goals

  • Extract game status logic into dedicated hook
  • Follow vim-maze architecture pattern
  • Clean separation of concerns
  • Reusable state machine

Tasks Completed

1. Created useGameStatus Module

File Structure:

src/useGameStatus/
├── index.ts              # Public exports
├── types.ts              # GameStatus, GameStatusManager
└── useGameStatus.ts      # Hook implementation

2. Implemented State Machine

States:

  • waiting - Initial state, awaiting start
  • started - Game in progress
  • paused - Game temporarily paused
  • game-over - Game lost
  • game-won - Game completed

State Transitions:

waiting ──startGame()──> started
               ▲            │
               │            │ togglePause()
               │            ▼
              quitGame()  paused
                           │
                           │ togglePause()
                           ▼
                        started

3. Hook API

const gameStatusManager = useGameStatus();
const {
  gameStatus,        // Current state
  setGameStatus,     // Direct setter (for special cases)
  startGame,         // waiting → started
  togglePause,       // started ↔ paused
  quitGame,          // * → waiting
} = gameStatusManager;

4. Updated useGame

  • Removed inline useState<GameStatus>
  • Uses useGameStatus() hook
  • Spreads gameStatusManager into GameManager
  • Cleaner, more maintainable code

5. Updated Type System

types.ts:

  • Imports GameStatus and GameStatusManager from useGameStatus
  • Re-exports for public API
  • GameManager extends GameStatusManager (intersection type)
  • Avoids duplication, single source of truth

Technical Details

Simpler than vim-maze:

  • No intermediate states (no 'has-key' equivalent)
  • Straightforward: waiting → started → won/over
  • Pause is optional toggle, not a separate flow

Hook Composition:

// In useGame
const gameStatusManager = useGameStatus();
const { gameStatus } = gameStatusManager;

// Compose into GameManager
const gameManager = {
  ...gameStatusManager,  // gameStatus, startGame, etc.
  // ... other managers
};

Type Safety:

  • GameManager extends GameStatusManager
  • All status methods available on gameManager
  • Full TypeScript inference throughout

Build Status

  • ✅ TypeScript compiles cleanly
  • ✅ Example app builds successfully
  • ✅ No breaking changes to public API

Next Steps

  • Phase 40: useCursor Hook Composition
    • useCellEditor (analog to useHero)
    • useVimMotions
    • useGameKeys
    • useCursorRender

Iteration 6: Phase 40.1 - Basic Cursor with hjkl Movement

Date: 2025-01-22

Goals

  • Implement basic cursor navigation
  • Add hjkl vim keys
  • Visual cursor highlight
  • Only active during gameplay

Tasks Completed

1. Created useCursor Module

  • useCursor.ts - Position tracking and movement logic
  • useCursorRender.ts - Visual cursor highlight on DOM
  • types.ts - Coord and CursorManager types

2. Cursor Implementation

  • Position tracking with useState (row/col 0-8)
  • Movement functions: moveLeft, moveRight, moveUp, moveDown
  • Bounds checking prevents moving outside 9x9 grid
  • Keyboard listener for h/j/k/l keys
  • Only active when gameStatus === 'started'

3. Visual Feedback

  • Blue highlight with border on current cell
  • CSS class 'cursor' applied to active cell
  • Removed from previous cell on movement

4. Fixed Initial Cursor Display (Issue 1)

Problem: Cursor not showing on game start before first move Root Cause: renderBoard() rebuilds DOM, wiping cursor class Solution: Added boardRendered counter

  • Increments after each renderBoard()
  • useCursorRender depends on boardRendered
  • Forces cursor re-application after DOM rebuild

Flow:

  1. Press Space → startGame()
  2. renderBoard() rebuilds DOM
  3. boardRendered increments
  4. useCursorRender triggers
  5. Cursor class applied to (0,0)

Technical Details

useState vs useRef:

  • Initially used useRef (no re-renders)
  • Changed to useState to trigger useCursorRender
  • Position changes now properly update DOM

Timing Issue Resolution:

  • renderBoard() is synchronous
  • useEffect runs after render
  • boardRendered ensures proper ordering

5. Fixed Cursor Border Conflicts (Issue 2)

Problem: Cursor border conflicting with 3x3 box separators

  • Cells have borders for grid structure
  • Cursor border overlapped/clipped at edges
  • Screenshots showed visual conflicts

Solution: Use outline instead of border

  • outline: 3px solid (sits above borders)
  • outline-offset: -3px (draws inside cell)
  • z-index: 10 + position: relative (layers above)
  • Outline doesn't affect layout or conflict with borders

6. Added Red Flash on Invalid Movement (Issue 3)

Feature: Visual feedback when attempting invalid moves

Implementation:

  • CSS: .invalid-move class with red outline and background
  • Animation: 500ms ease-out fade from bright to transparent
  • Logic: flashInvalidMove() triggers on bounds violations
  • Automatically removes class after 500ms

User Experience:

  • Press h at left edge → Red flash
  • Press j at bottom → Red flash
  • Press k at top → Red flash
  • Press l at right → Red flash

Cleanup:

  • Removed all console.log debugging statements
  • Production-ready code

Summary

Phase 40.1 complete with all issues resolved:

  • ✅ Basic cursor with hjkl movement
  • ✅ Visual cursor highlight (blue outline)
  • ✅ Shows immediately on game start
  • ✅ No border conflicts with grid
  • ✅ Red flash on invalid movement
  • ✅ Clean, production-ready code

Iteration 7: Phase 40.2 - Complete Vim Motion System

Date: 2025-01-22

Goals

  • Implement anchor moves (0, ^, $, gg, G)
  • Add count prefixes (5j, 10l, etc.)
  • Add repeat motion (.)
  • Handle the "0 caveat" properly

Tasks Completed

1. Created useVimMotions Hook

Complete vim motion processor with state management:

  • Count buffer tracking (countRef)
  • Last motion tracking for repeat
  • Pending g state for gg sequence

2. Implemented Anchor Moves

Horizontal (Row-based):

  • 0 or ^ → Start of row (col 0)
  • $ → End of row (col 8)

Vertical (Column-based):

  • gg → Top of column (row 0)
  • G → Bottom of column (row 8)

3. Count Prefix Support

  • Works with all directional moves: h, j, k, l
  • Examples: 5j, 10l, 3k, 2h
  • Bounded to grid limits (0-8)
  • Red flash on invalid moves

4. Repeat Motion

  • . → Repeats last motion with same count
  • Tracks motion direction and step count
  • Works with both direct moves and counted moves

5. Solved the "0 Caveat"

Problem: 0 needs to work as both anchor AND count digit

  • 0 alone → anchor to start
  • 10j → count of 10

Solution (from vim-maze):

if (key === '0' && countRef.current === '') {
  return { type: 'anchor', data: 'start' };  // Anchor
}
countRef.current += key;  // Count digit

Logic: Check if count buffer is empty

  • Empty → 0 is anchor
  • Not empty → 0 is count digit

6. Fixed gg Two-Key Sequence

Bug: Single g was triggering immediately

Solution: Added pendingGRef state

  • First g → Set flag, wait for second key
  • Second g → Trigger "top" anchor
  • Any other key → Clear flag, no action

Technical Implementation

processKey Return Types:

{ type: 'move', data: Motion }      // h/j/k/l with count
{ type: 'anchor', data: string }    // 0/^/$/gg/G
{ type: 'repeat', data: Motion }    // .
{ type: 'count', data: string }     // 0-9
null                                 // Ignored key or pending state

Motion Tracking:

type Motion = { dr: number; dc: number; steps: number };
// Example: 5j → { dr: 1, dc: 0, steps: 5 }

State Management:

  • countRef → Accumulates count digits
  • lastMotionRef → Stores last motion for repeat
  • pendingGRef → Tracks first g press for gg

Testing Results

✅ All anchor moves working ✅ Count prefixes working (5j, 10l, etc.) ✅ Repeat motion (.) working ✅ 0 works as both anchor and count ✅ gg requires two presses ✅ All moves bounded with red flash feedback

Next Steps

  • Phase 40.3: Implement cell editing (i/r modes)
  • Add digit entry (1-9)
  • Add erase (x, Backspace)
  • Mode indicators

Iteration 8: Phase 40.3 - Edit Mode Implementation

Date: 2025-01-22

Goals

  • Implement edit mode state machine
  • Add rules for editable vs given cells
  • Use existing CSS classes for visual feedback
  • No digit entry yet - just mode switching

Tasks Completed

1. Mode State Machine (Phase 40.3.1)

Added CursorMode type:

  • 'normal' - Navigation mode (vim motions active)
  • 'edit' - Edit mode (ready for digit entry)

Key Bindings:

  • i - Enter edit mode (insert)
  • Escape - Exit edit mode to normal

State Management:

  • Mode tracked with useState in useCursor
  • Starts in 'normal' mode on game start
  • Mode passed to useCursorRender for visual feedback

2. Edit Mode Rules (Phase 40.3.2)

Cell Editability:

  • Given cells (.given class): Cannot be edited
  • Empty cells: Can be edited
  • User-entered cells (.user-entered class): Can be edited

Validation Logic:

if (key === 'i') {
  const targetCell = cells[index];
  if (targetCell && targetCell.classList.contains('given')) {
    flashInvalidMove();  // Red flash
    return;              // Block edit mode
  }
  setMode('edit');       // Allow edit mode
}

Visual Feedback:

  • Attempt to edit given cell → Red flash (500ms)
  • Successfully enter edit mode → Yellow/amber glow

3. Visual Indicators

Using Existing CSS Classes:

  • Normal mode: .active class (blue glow)
  • Edit mode: .editing class (yellow/amber pulsing glow)
  • Invalid attempt: .invalid-move class (red flash)

Removed Custom CSS:

  • Removed .cursor and .cursor-edit classes
  • Using original CSS design from src_old
  • Consistent with existing game aesthetics

4. Mode Behavior

Normal Mode:

  • All vim motions work (hjkl, anchors, counts, repeat)
  • Blue glow on current cell
  • Can navigate freely

Edit Mode:

  • All vim motions blocked
  • Yellow/amber pulsing glow
  • Only Escape key active (exit to normal)
  • Ready for digit entry (not implemented yet)

Technical Details

Cell Classification:

  • Given cells: Have digits from puzzle generation
  • Renderer adds .given class to these cells
  • These cells are immutable (cannot be edited)

Flash Mechanism:

  • flashInvalidMove() adds .invalid-move class
  • CSS animation shows red glow for 500ms
  • Class auto-removed after timeout
  • Same mechanism used for boundary violations

Mode Transition Flow:

Normal (blue) 
   ↓ [press i on empty/user cell]
Edit (yellow)
   ↓ [press Escape]
Normal (blue)

Normal (blue)
   ↓ [press i on given cell]
Red flash → Stay in Normal

Testing Results

✅ Edit mode state machine working ✅ i and Escape toggle modes correctly ✅ Given cells block edit mode with red flash ✅ Empty cells allow edit mode ✅ User-entered cells allow edit mode ✅ Vim motions blocked in edit mode ✅ Visual indicators clear and distinct

Next Steps

  • Phase 40.3.3: Add digit entry (1-9)
  • Phase 40.3.4: Add erase functionality (x, Backspace)
  • Phase 40.3.5: Add replace mode (r)
  • Integration with board state management

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:

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:

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...