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) <noreply@anthropic.com>
This commit is contained in:
thibaud-leclere
2026-03-30 21:59:11 +02:00
parent ba9a3fba5d
commit 335a55562f
5 changed files with 99 additions and 19 deletions

View File

@@ -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);

View File

@@ -5,15 +5,34 @@ export default function GameGrid({ grid, width, middle }) {
return (
<table id="actors">
<tbody>
{grid.map((row, rowIndex) => (
<GameRow
key={rowIndex}
actorName={row.actorName}
pos={row.pos}
colStart={middle - row.pos}
totalWidth={width}
/>
))}
{grid.map((row, rowIndex) => {
if (row.separator !== undefined) {
return (
<tr key={rowIndex} className="separator-row">
<td />
{Array.from({ length: middle }, (_, i) => (
<td key={i} />
))}
<td className="letter-static separator-char">
{row.separator === ' ' ? '' : row.separator}
</td>
{Array.from({ length: width - middle }, (_, i) => (
<td key={middle + 1 + i} />
))}
</tr>
);
}
return (
<GameRow
key={rowIndex}
actorName={row.actorName}
pos={row.pos}
colStart={middle - row.pos}
totalWidth={width}
/>
);
})}
</tbody>
</table>
);

View File

@@ -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 (
<tr>
@@ -28,13 +40,23 @@ export default function GameRow({ actorName, pos, colStart, totalWidth }) {
return <td key={colIndex} />;
}
const ch = letters[charIndex];
if (!isLetter(ch)) {
return (
<td key={colIndex} className="letter-static">
{ch}
</td>
);
}
return (
<LetterInput
key={colIndex}
highlighted={charIndex === pos}
inputRef={setInputRef(charIndex)}
onNext={() => focusInput(charIndex + 1)}
onPrev={() => focusInput(charIndex - 1)}
onNext={() => focusNextInput(charIndex, 1)}
onPrev={() => focusNextInput(charIndex, -1)}
/>
);
})}

View File

@@ -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 {