From 335a55562f28b6b7d0147a806c74c6b61cbdfc3b Mon Sep 17 00:00:00 2001 From: thibaud-leclere Date: Mon, 30 Mar 2026 21:59:11 +0200 Subject: [PATCH] feat: handle non-letter characters in actor names with separator rows Display spaces, hyphens and other non-letter characters as static cells instead of input fields, and add separator rows in the grid for non-alphabetic characters in the main actor's name. Co-Authored-By: Claude Opus 4.6 (1M context) --- assets/react/controllers/ActorPopover.jsx | 2 +- assets/react/controllers/GameGrid.jsx | 37 ++++++++++++++++------ assets/react/controllers/GameRow.jsx | 38 ++++++++++++++++++----- assets/styles/app.css | 21 +++++++++++++ src/Service/GameGridGenerator.php | 20 +++++++++++- 5 files changed, 99 insertions(+), 19 deletions(-) diff --git a/assets/react/controllers/ActorPopover.jsx b/assets/react/controllers/ActorPopover.jsx index 61e3022..3941c4c 100644 --- a/assets/react/controllers/ActorPopover.jsx +++ b/assets/react/controllers/ActorPopover.jsx @@ -8,7 +8,7 @@ export default function ActorPopover({ actorName }) { open: isOpen, onOpenChange: setIsOpen, middleware: [offset(8), flip(), shift()], - placement: 'top', + placement: 'left', }); const click = useClick(context); diff --git a/assets/react/controllers/GameGrid.jsx b/assets/react/controllers/GameGrid.jsx index 73cb81a..5c91d9d 100644 --- a/assets/react/controllers/GameGrid.jsx +++ b/assets/react/controllers/GameGrid.jsx @@ -5,15 +5,34 @@ export default function GameGrid({ grid, width, middle }) { return ( - {grid.map((row, rowIndex) => ( - - ))} + {grid.map((row, rowIndex) => { + if (row.separator !== undefined) { + return ( + + + {Array.from({ length: width - middle }, (_, i) => ( + + ); + } + + return ( + + ); + })}
+ {Array.from({ length: middle }, (_, i) => ( + + ))} + + {row.separator === ' ' ? '' : row.separator} + + ))} +
); diff --git a/assets/react/controllers/GameRow.jsx b/assets/react/controllers/GameRow.jsx index 6caa8e0..8373562 100644 --- a/assets/react/controllers/GameRow.jsx +++ b/assets/react/controllers/GameRow.jsx @@ -1,19 +1,31 @@ -import React, { useRef, useCallback } from 'react'; +import React, { useRef, useCallback, useMemo } from 'react'; import LetterInput from './LetterInput'; import ActorPopover from './ActorPopover'; +function isLetter(ch) { + return /[a-zA-Z]/.test(ch); +} + export default function GameRow({ actorName, pos, colStart, totalWidth }) { const inputRefs = useRef([]); + const letters = actorName.split(''); + + const letterIndices = useMemo( + () => letters.reduce((acc, ch, i) => { if (isLetter(ch)) acc.push(i); return acc; }, []), + [actorName] + ); const setInputRef = useCallback((index) => (el) => { inputRefs.current[index] = el; }, []); - const focusInput = useCallback((index) => { - inputRefs.current[index]?.focus(); - }, []); - - const letters = actorName.split(''); + const focusNextInput = useCallback((charIndex, direction) => { + const currentPos = letterIndices.indexOf(charIndex); + const nextPos = currentPos + direction; + if (nextPos >= 0 && nextPos < letterIndices.length) { + inputRefs.current[letterIndices[nextPos]]?.focus(); + } + }, [letterIndices]); return ( @@ -28,13 +40,23 @@ export default function GameRow({ actorName, pos, colStart, totalWidth }) { return ; } + const ch = letters[charIndex]; + + if (!isLetter(ch)) { + return ( + + {ch} + + ); + } + return ( focusInput(charIndex + 1)} - onPrev={() => focusInput(charIndex - 1)} + onNext={() => focusNextInput(charIndex, 1)} + onPrev={() => focusNextInput(charIndex, -1)} /> ); })} diff --git a/assets/styles/app.css b/assets/styles/app.css index cd0252d..26fefe7 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -75,6 +75,27 @@ body { color: var(--orange); } +.letter-static { + width: 38px; + height: 38px; + text-align: center; + font-family: 'Inter', sans-serif; + font-size: 15px; + font-weight: 700; + text-transform: uppercase; + color: var(--text); + vertical-align: middle; +} + +.separator-row td { + height: 12px; + padding: 0; +} + +.separator-char { + height: 12px; +} + /* ── Popover ── */ .popover-trigger { diff --git a/src/Service/GameGridGenerator.php b/src/Service/GameGridGenerator.php index 564069f..d13cb57 100644 --- a/src/Service/GameGridGenerator.php +++ b/src/Service/GameGridGenerator.php @@ -71,7 +71,25 @@ class GameGridGenerator $rightSize = 0; $grid = []; - foreach ($game->getRows() as $row) { + $mainActorChars = str_split($game->getMainActor()->getName()); + $rows = $game->getRows()->toArray(); + $rowIndex = 0; + + foreach ($mainActorChars as $char) { + if (!preg_match('/[a-zA-Z]/', $char)) { + $grid[] = [ + 'separator' => $char, + ]; + continue; + } + + $row = $rows[$rowIndex] ?? null; + ++$rowIndex; + + if ($row === null) { + continue; + } + $actor = $row->getActor(); $pos = $row->getPosition();