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.
This commit is contained in:
+17
-1
@@ -255,7 +255,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Cursor highlight */
|
||||
/* Cursor highlight - Normal mode */
|
||||
.sudoku-cell.cursor {
|
||||
background-color: rgba(59, 130, 246, 0.3);
|
||||
outline: 3px solid rgb(59, 130, 246);
|
||||
@@ -264,6 +264,22 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Cursor highlight - Edit mode */
|
||||
.sudoku-cell.cursor-edit {
|
||||
background-color: rgba(34, 197, 94, 0.3);
|
||||
outline: 3px solid rgb(34, 197, 94);
|
||||
animation: edit-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes edit-pulse {
|
||||
0%, 100% {
|
||||
outline-color: rgb(34, 197, 94);
|
||||
}
|
||||
50% {
|
||||
outline-color: rgb(74, 222, 128);
|
||||
}
|
||||
}
|
||||
|
||||
/* Invalid move flash */
|
||||
.sudoku-cell.invalid-move {
|
||||
background-color: rgba(239, 68, 68, 0.4);
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { useCursor } from './useCursor';
|
||||
export type { CursorManager, Coord } from './types';
|
||||
export type { CursorManager, Coord, CursorMode } from './types';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export type Coord = { row: number; col: number };
|
||||
|
||||
export type CursorMode = 'normal' | 'edit';
|
||||
|
||||
export type Motion = { dr: number; dc: number; steps: number };
|
||||
|
||||
export type VimMotionSystem = {
|
||||
@@ -13,6 +15,7 @@ export type VimMotionSystem = {
|
||||
|
||||
export type CursorManager = {
|
||||
position: () => Coord;
|
||||
mode: () => CursorMode;
|
||||
moveLeft: (count?: number) => void;
|
||||
moveRight: (count?: number) => void;
|
||||
moveUp: (count?: number) => void;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { Coord, CursorManager } from './types';
|
||||
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();
|
||||
|
||||
@@ -92,6 +93,19 @@ export function useCursor(gameStatusManager: GameStatusManager, containerRef: Re
|
||||
|
||||
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;
|
||||
@@ -131,10 +145,11 @@ export function useCursor(gameStatusManager: GameStatusManager, containerRef: Re
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [gameStatus]);
|
||||
}, [gameStatus, mode]);
|
||||
|
||||
const cursor: CursorManager = {
|
||||
position: () => position,
|
||||
mode: () => mode,
|
||||
moveLeft,
|
||||
moveRight,
|
||||
moveUp,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useEffect } from 'react';
|
||||
import type { Coord } from './types';
|
||||
import type { Coord, CursorMode } from './types';
|
||||
import type { BoardManager } from '../useBoard/types';
|
||||
import type { GameStatus } from '../useGameStatus/types';
|
||||
|
||||
export function useCursorRender(
|
||||
containerRef: BoardManager['containerRef'],
|
||||
position: Coord,
|
||||
mode: CursorMode,
|
||||
gameStatus: GameStatus,
|
||||
boardRendered: number
|
||||
) {
|
||||
@@ -18,14 +19,18 @@ export function useCursorRender(
|
||||
const cells = container.querySelectorAll('.sudoku-cell');
|
||||
|
||||
cells.forEach(cell => {
|
||||
cell.classList.remove('cursor');
|
||||
cell.classList.remove('cursor', 'cursor-edit');
|
||||
});
|
||||
|
||||
const index = position.row * 9 + position.col;
|
||||
const targetCell = cells[index];
|
||||
|
||||
if (targetCell) {
|
||||
targetCell.classList.add('cursor');
|
||||
if (mode === 'edit') {
|
||||
targetCell.classList.add('cursor', 'cursor-edit');
|
||||
} else {
|
||||
targetCell.classList.add('cursor');
|
||||
}
|
||||
}
|
||||
}, [position, containerRef, gameStatus, boardRendered]);
|
||||
}, [position, mode, containerRef, gameStatus, boardRendered]);
|
||||
}
|
||||
|
||||
+2
-1
@@ -18,8 +18,9 @@ export function useGame(options: GameOptions = {}, platformHook?: unknown): Game
|
||||
|
||||
const cursor = useCursor(gameStatusManager, containerRef);
|
||||
const cursorPosition = cursor.position();
|
||||
const cursorMode = cursor.mode();
|
||||
|
||||
useCursorRender(containerRef, cursorPosition, gameStatus, boardRendered);
|
||||
useCursorRender(containerRef, cursorPosition, cursorMode, gameStatus, boardRendered);
|
||||
|
||||
useEffect(() => {
|
||||
renderBoard();
|
||||
|
||||
Reference in New Issue
Block a user