Files
vim-sudoku/src_failed/useCursor/useCursor.ts
T
pastilhas 9292b83dd0 feat: prepare vim-sudoku for unified API implementation
- Archive previous implementation attempt (src -> src_failed)
- Create comprehensive IMPLEMENTATION_PLAN.md with phased approach
- Create IMPLEMENTATION.md to track implementation progress
- Update example app to match vim-maze patterns:
  - Simplify UI to desktop-only (no mobile support)
  - Add score metrics display (time, keystrokes, final score)
  - Update instructions for sudoku-specific keys (hjkl, i for edit)
  - Remove unnecessary mobile hooks

Phase 5 complete. Ready to begin Phase 10 (Setup & Core Types).
2025-10-22 18:08:33 +01:00

275 lines
7.5 KiB
TypeScript

import { useState, useCallback, useEffect } from 'react';
import type { BoardManager } from '../useBoard/types';
import type { CursorManager, CursorMode, Coord, VimMotionSystem, GameKeyManager, KeyLogEntry } from './types';
import { useVimMotions } from './useVimMotions';
export function useCursor(board: BoardManager): CursorManager & { keyManager: GameKeyManager } {
const [position, setPosition] = useState<Coord>({ row: 0, col: 0 });
const [mode, setMode] = useState<CursorMode>('normal');
const [keyLog, setKeyLog] = useState<KeyLogEntry[]>([]);
const vimMotions = useVimMotions();
const addKeyToLog = useCallback((key: string) => {
setKeyLog(prev => [...prev, { key, timestamp: Date.now() }]);
}, []);
const move = useCallback((dRows: number, dCols: number, count: number) => {
setPosition(prev => {
const newRow = prev.row + (dRows * count);
const newCol = prev.col + (dCols * count);
if (newRow < 0 || newRow > 8 || newCol < 0 || newCol > 8) {
flashInvalid();
return prev;
}
return { row: newRow, col: newCol };
});
}, []);
const moveLeft = useCallback((count = 1) => {
move(0, -1, count);
}, [move]);
const moveRight = useCallback((count = 1) => {
move(0, 1, count);
}, [move]);
const moveUp = useCallback((count = 1) => {
move(-1, 0, count);
}, [move]);
const moveDown = useCallback((count = 1) => {
move(1, 0, count);
}, [move]);
const moveToStart = useCallback(() => {
setPosition(prev => ({ ...prev, col: 0 }));
}, []);
const moveToEnd = useCallback(() => {
setPosition(prev => ({ ...prev, col: 8 }));
}, []);
const moveToTop = useCallback(() => {
setPosition({ row: 0, col: 0 });
}, []);
const moveToBottom = useCallback(() => {
setPosition({ row: 8, col: 0 });
}, []);
const repeatLastMotion = useCallback(() => {
const lastMotion = vimMotions.repeatLastMotion();
if (lastMotion) {
move(lastMotion.dr, lastMotion.dc, lastMotion.steps);
}
}, [vimMotions, move]);
const resetCount = useCallback(() => {
vimMotions.resetCount();
}, [vimMotions]);
const getCount = useCallback(() => {
return vimMotions.getCount().toString();
}, [vimMotions]);
const hasCount = useCallback(() => {
return vimMotions.hasCount();
}, [vimMotions]);
const setCount = useCallback((digit: string) => {
vimMotions.processKey(digit);
}, [vimMotions]);
const setLastKey = useCallback((key: string) => {
addKeyToLog(key);
}, [addKeyToLog]);
const getLastKey = useCallback(() => {
return keyLog[keyLog.length - 1]?.key || '';
}, [keyLog]);
const flashInvalid = useCallback(() => {
const container = board.containerRef.current;
if (!container) return;
const cellEl = container.querySelector(`.sudoku-cell[data-row="${position.row}"][data-col="${position.col}"]`) as HTMLElement;
if (cellEl) {
cellEl.classList.add('invalid-move');
setTimeout(() => cellEl.classList.remove('invalid-move'), 180);
}
}, [board, position]);
const flashValid = useCallback(() => {
const container = board.containerRef.current;
if (!container) return;
const cellEl = container.querySelector(`.sudoku-cell[data-row="${position.row}"][data-col="${position.col}"]`) as HTMLElement;
if (cellEl) {
cellEl.classList.add('valid-move');
setTimeout(() => cellEl.classList.remove('valid-move'), 300);
}
}, [board, position]);
const flashLocked = useCallback(() => {
const container = board.containerRef.current;
if (!container) return;
const cellEl = container.querySelector(`.sudoku-cell[data-row="${position.row}"][data-col="${position.col}"]`) as HTMLElement;
if (cellEl) {
cellEl.classList.add('locked-cell');
setTimeout(() => cellEl.classList.remove('locked-cell'), 300);
}
}, [board, position]);
const highlightConflicts = useCallback((conflictIndices: number[]) => {
const container = board.containerRef.current;
if (!container) return;
conflictIndices.forEach((idx) => {
const row = Math.floor(idx / 9);
const col = idx % 9;
const cellEl = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement;
if (cellEl) {
cellEl.classList.add('conflict');
}
});
setTimeout(() => {
conflictIndices.forEach((idx) => {
const row = Math.floor(idx / 9);
const col = idx % 9;
const cellEl = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement;
if (cellEl) {
cellEl.classList.remove('conflict');
}
});
}, 2000);
}, [board]);
const clearKeyLog = useCallback(() => {
setKeyLog([]);
}, []);
const getKeyLog = useCallback(() => {
return [...keyLog];
}, [keyLog]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const key = e.key;
const result = vimMotions.processKey(key);
if (!result) return;
addKeyToLog(key);
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 'mode':
setMode(result.data);
break;
case 'digit':
if (mode === 'edit') {
const success = board.setCell(position.row, position.col, result.data);
if (success) {
flashValid();
} else if (!board.canEditCell(position.row, position.col)) {
flashLocked();
} else {
const conflicts = board.getConflicts(position.row, position.col);
if (conflicts.length > 0) {
highlightConflicts(conflicts);
}
flashInvalid();
}
}
break;
case 'erase':
if (mode === 'edit') {
const success = board.setCell(position.row, position.col, '.');
if (success) {
flashValid();
} else if (!board.canEditCell(position.row, position.col)) {
flashLocked();
} else {
flashInvalid();
}
}
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [vimMotions, addKeyToLog, move, moveToStart, moveToEnd, moveToTop, moveToBottom, mode, board, position, flashValid, flashLocked, flashInvalid, highlightConflicts]);
const cursorManager: CursorManager = {
position: () => position,
mode: () => mode,
move,
moveLeft,
moveRight,
moveUp,
moveDown,
moveToStart,
moveToEnd,
moveToTop,
moveToBottom,
repeatLastMotion,
resetCount,
getCount,
hasCount,
setCount,
setLastKey,
getLastKey,
setMode,
flashInvalid,
flashValid,
flashLocked,
highlightConflicts
};
const keyManager: GameKeyManager = {
keyLog,
clearKeyLog,
getKeyLog,
editMode: mode,
setEditMode: setMode
};
return {
...cursorManager,
keyManager
};
}