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 {
|
.sudoku-cell.cursor {
|
||||||
background-color: rgba(59, 130, 246, 0.3);
|
background-color: rgba(59, 130, 246, 0.3);
|
||||||
outline: 3px solid rgb(59, 130, 246);
|
outline: 3px solid rgb(59, 130, 246);
|
||||||
@@ -264,6 +264,22 @@
|
|||||||
position: relative;
|
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 */
|
/* Invalid move flash */
|
||||||
.sudoku-cell.invalid-move {
|
.sudoku-cell.invalid-move {
|
||||||
background-color: rgba(239, 68, 68, 0.4);
|
background-color: rgba(239, 68, 68, 0.4);
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { useCursor } from './useCursor';
|
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 Coord = { row: number; col: number };
|
||||||
|
|
||||||
|
export type CursorMode = 'normal' | 'edit';
|
||||||
|
|
||||||
export type Motion = { dr: number; dc: number; steps: number };
|
export type Motion = { dr: number; dc: number; steps: number };
|
||||||
|
|
||||||
export type VimMotionSystem = {
|
export type VimMotionSystem = {
|
||||||
@@ -13,6 +15,7 @@ export type VimMotionSystem = {
|
|||||||
|
|
||||||
export type CursorManager = {
|
export type CursorManager = {
|
||||||
position: () => Coord;
|
position: () => Coord;
|
||||||
|
mode: () => CursorMode;
|
||||||
moveLeft: (count?: number) => void;
|
moveLeft: (count?: number) => void;
|
||||||
moveRight: (count?: number) => void;
|
moveRight: (count?: number) => void;
|
||||||
moveUp: (count?: number) => void;
|
moveUp: (count?: number) => void;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useState, useEffect } from 'react';
|
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 type { GameStatusManager } from '../useGameStatus/types';
|
||||||
import { useVimMotions } from './useVimMotions';
|
import { useVimMotions } from './useVimMotions';
|
||||||
|
|
||||||
export function useCursor(gameStatusManager: GameStatusManager, containerRef: React.RefObject<HTMLDivElement | null>) {
|
export function useCursor(gameStatusManager: GameStatusManager, containerRef: React.RefObject<HTMLDivElement | null>) {
|
||||||
const [position, setPosition] = useState<Coord>({ row: 0, col: 0 });
|
const [position, setPosition] = useState<Coord>({ row: 0, col: 0 });
|
||||||
|
const [mode, setMode] = useState<CursorMode>('normal');
|
||||||
const { gameStatus } = gameStatusManager;
|
const { gameStatus } = gameStatusManager;
|
||||||
const vimMotions = useVimMotions();
|
const vimMotions = useVimMotions();
|
||||||
|
|
||||||
@@ -92,6 +93,19 @@ export function useCursor(gameStatusManager: GameStatusManager, containerRef: Re
|
|||||||
|
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
const key = e.key;
|
const key = e.key;
|
||||||
|
|
||||||
|
if (mode === 'edit') {
|
||||||
|
if (key === 'Escape') {
|
||||||
|
setMode('normal');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'i') {
|
||||||
|
setMode('edit');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const result = vimMotions.processKey(key);
|
const result = vimMotions.processKey(key);
|
||||||
|
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
@@ -131,10 +145,11 @@ export function useCursor(gameStatusManager: GameStatusManager, containerRef: Re
|
|||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [gameStatus]);
|
}, [gameStatus, mode]);
|
||||||
|
|
||||||
const cursor: CursorManager = {
|
const cursor: CursorManager = {
|
||||||
position: () => position,
|
position: () => position,
|
||||||
|
mode: () => mode,
|
||||||
moveLeft,
|
moveLeft,
|
||||||
moveRight,
|
moveRight,
|
||||||
moveUp,
|
moveUp,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import type { Coord } from './types';
|
import type { Coord, CursorMode } from './types';
|
||||||
import type { BoardManager } from '../useBoard/types';
|
import type { BoardManager } from '../useBoard/types';
|
||||||
import type { GameStatus } from '../useGameStatus/types';
|
import type { GameStatus } from '../useGameStatus/types';
|
||||||
|
|
||||||
export function useCursorRender(
|
export function useCursorRender(
|
||||||
containerRef: BoardManager['containerRef'],
|
containerRef: BoardManager['containerRef'],
|
||||||
position: Coord,
|
position: Coord,
|
||||||
|
mode: CursorMode,
|
||||||
gameStatus: GameStatus,
|
gameStatus: GameStatus,
|
||||||
boardRendered: number
|
boardRendered: number
|
||||||
) {
|
) {
|
||||||
@@ -18,14 +19,18 @@ export function useCursorRender(
|
|||||||
const cells = container.querySelectorAll('.sudoku-cell');
|
const cells = container.querySelectorAll('.sudoku-cell');
|
||||||
|
|
||||||
cells.forEach(cell => {
|
cells.forEach(cell => {
|
||||||
cell.classList.remove('cursor');
|
cell.classList.remove('cursor', 'cursor-edit');
|
||||||
});
|
});
|
||||||
|
|
||||||
const index = position.row * 9 + position.col;
|
const index = position.row * 9 + position.col;
|
||||||
const targetCell = cells[index];
|
const targetCell = cells[index];
|
||||||
|
|
||||||
if (targetCell) {
|
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 cursor = useCursor(gameStatusManager, containerRef);
|
||||||
const cursorPosition = cursor.position();
|
const cursorPosition = cursor.position();
|
||||||
|
const cursorMode = cursor.mode();
|
||||||
|
|
||||||
useCursorRender(containerRef, cursorPosition, gameStatus, boardRendered);
|
useCursorRender(containerRef, cursorPosition, cursorMode, gameStatus, boardRendered);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
renderBoard();
|
renderBoard();
|
||||||
|
|||||||
Reference in New Issue
Block a user