Files
vim-sudoku/src/useCursor/useCursor.ts
T
pastilhas 0ce1d7fe65 feat: implement edit mode state machine (i and Escape)
Added mode state machine with visual feedback:

Mode State:
- CursorMode type: 'normal' | 'edit'
- Tracked in useCursor with useState
- Starts in 'normal' mode

Key Bindings:
- Press 'i' in normal mode → Enter edit mode
- Press 'Escape' in edit mode → Return to normal mode
- All vim motions (hjkl, anchors, etc.) only work in normal mode
- Edit mode blocks all movement keys

Visual Feedback:
- Normal mode: Blue outline (rgb(59, 130, 246))
- Edit mode: Green pulsing outline (rgb(34, 197, 94))
- Smooth pulse animation (1.5s ease-in-out infinite)
- cursor-edit CSS class applied in edit mode

Flow:
1. Game starts in normal mode (blue cursor)
2. Press 'i' → Green pulsing cursor (edit mode)
3. Press 'Escape' → Blue cursor (normal mode)

No digit entry yet - just state machine and visual indicators.
Phase 40.3.1 complete.
2025-10-22 19:22:15 +01:00

166 lines
3.9 KiB
TypeScript

import { useState, useEffect } from 'react';
import type { Coord, CursorManager, CursorMode } from './types';
import type { GameStatusManager } from '../useGameStatus/types';
import { useVimMotions } from './useVimMotions';
export function useCursor(gameStatusManager: GameStatusManager, containerRef: React.RefObject<HTMLDivElement | null>) {
const [position, setPosition] = useState<Coord>({ row: 0, col: 0 });
const [mode, setMode] = useState<CursorMode>('normal');
const { gameStatus } = gameStatusManager;
const vimMotions = useVimMotions();
function flashInvalidMove(): void {
const container = containerRef.current;
if (!container) return;
const cells = container.querySelectorAll('.sudoku-cell');
const index = position.row * 9 + position.col;
const targetCell = cells[index];
if (targetCell) {
targetCell.classList.add('invalid-move');
setTimeout(() => {
targetCell.classList.remove('invalid-move');
}, 500);
}
}
function move(dRows: number, dCols: number, count: number = 1): void {
setPosition(currentPos => {
const newRow = currentPos.row + (dRows * count);
const newCol = currentPos.col + (dCols * count);
if (newRow < 0 || newRow > 8 || newCol < 0 || newCol > 8) {
flashInvalidMove();
return currentPos;
}
return { row: newRow, col: newCol };
});
}
function moveLeft(count = 1): void {
move(0, -1, count);
}
function moveRight(count = 1): void {
move(0, 1, count);
}
function moveUp(count = 1): void {
move(-1, 0, count);
}
function moveDown(count = 1): void {
move(1, 0, count);
}
function moveToStart(): void {
setPosition(prev => ({ ...prev, col: 0 }));
}
function moveToEnd(): void {
setPosition(prev => ({ ...prev, col: 8 }));
}
function moveToTop(): void {
setPosition(prev => ({ row: 0, col: prev.col }));
}
function moveToBottom(): void {
setPosition(prev => ({ row: 8, col: prev.col }));
}
function repeatLastMotion(): void {
const motion = vimMotions.repeatLastMotion();
if (!motion) return;
setPosition(currentPos => {
const newRow = currentPos.row + (motion.dr * motion.steps);
const newCol = currentPos.col + (motion.dc * motion.steps);
if (newRow < 0 || newRow > 8 || newCol < 0 || newCol > 8) {
flashInvalidMove();
return currentPos;
}
return { row: newRow, col: newCol };
});
}
useEffect(() => {
if (gameStatus !== 'started') return;
function handleKeyDown(e: KeyboardEvent) {
const key = e.key;
if (mode === 'edit') {
if (key === 'Escape') {
setMode('normal');
}
return;
}
if (key === 'i') {
setMode('edit');
return;
}
const result = vimMotions.processKey(key);
if (!result) return;
switch (result.type) {
case 'move':
move(result.data.dr, result.data.dc, result.data.steps);
break;
case 'anchor':
switch (result.data) {
case 'start':
moveToStart();
break;
case 'end':
moveToEnd();
break;
case 'top':
moveToTop();
break;
case 'bottom':
moveToBottom();
break;
}
break;
case 'repeat':
if (result.data) {
move(result.data.dr, result.data.dc, result.data.steps);
}
break;
case 'count':
break;
}
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [gameStatus, mode]);
const cursor: CursorManager = {
position: () => position,
mode: () => mode,
moveLeft,
moveRight,
moveUp,
moveDown,
moveToStart,
moveToEnd,
moveToTop,
moveToBottom,
repeatLastMotion,
};
return cursor;
}