From f6d180474a6dffcdc4946e92347799326c479ba9 Mon Sep 17 00:00:00 2001 From: thibaud-leclere Date: Tue, 31 Mar 2026 14:17:22 +0200 Subject: [PATCH] feat: responsive grid with scrollable area and fixed hint column - Add CSS variables (--cell, --cell-font, --trigger-h) for responsive cell sizing - Shrink grid cells at 600px and 420px breakpoints - Add .page-body wrapper with 16px horizontal padding to prevent edge collisions - Separate hint column from scrollable grid: hints rendered outside the table in a fixed flex column, only the letter grid scrolls horizontally Co-Authored-By: Claude Sonnet 4.6 --- assets/react/controllers/GameGrid.jsx | 70 ++++++++++++-------- assets/react/controllers/GameRow.jsx | 6 +- assets/styles/app.css | 95 ++++++++++++++++++++++----- templates/base.html.twig | 4 +- 4 files changed, 124 insertions(+), 51 deletions(-) diff --git a/assets/react/controllers/GameGrid.jsx b/assets/react/controllers/GameGrid.jsx index 43777f5..b9de1c7 100644 --- a/assets/react/controllers/GameGrid.jsx +++ b/assets/react/controllers/GameGrid.jsx @@ -1,41 +1,55 @@ import React from 'react'; import GameRow from './GameRow'; +import ActorPopover from './ActorPopover'; export default function GameGrid({ grid, width, middle }) { return ( - - +
+
{grid.map((row, rowIndex) => { if (row.separator !== undefined) { - return ( -
- - {Array.from({ length: width - middle }, (_, i) => ( - - ); + return
; } - return ( - +
+ +
); })} -
-
- {Array.from({ length: middle }, (_, i) => ( - - ))} - - {row.separator === ' ' ? '' : row.separator} - - ))} -
+ +
+ + + {grid.map((row, rowIndex) => { + if (row.separator !== undefined) { + return ( + + {Array.from({ length: middle }, (_, i) => ( + + {Array.from({ length: width - middle }, (_, i) => ( + + ); + } + + return ( + + ); + })} + +
+ ))} + + {row.separator === ' ' ? '' : row.separator} + + ))} +
+
+ ); } diff --git a/assets/react/controllers/GameRow.jsx b/assets/react/controllers/GameRow.jsx index ed85714..3c505c5 100644 --- a/assets/react/controllers/GameRow.jsx +++ b/assets/react/controllers/GameRow.jsx @@ -1,12 +1,11 @@ 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, hintType, hintText }) { +export default function GameRow({ actorName, pos, colStart, totalWidth }) { const inputRefs = useRef([]); const letters = actorName.split(''); @@ -29,9 +28,6 @@ export default function GameRow({ actorName, pos, colStart, totalWidth, hintType return ( - - - {Array.from({ length: totalWidth + 1 }, (_, colIndex) => { const charIndex = colIndex - colStart; const isInRange = charIndex >= 0 && charIndex < letters.length; diff --git a/assets/styles/app.css b/assets/styles/app.css index b3dc066..61d7811 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -21,6 +21,9 @@ --radius-sm: 6px; --radius-md: 10px; --radius-lg: 18px; + --cell: 38px; + --cell-font: 15px; + --trigger-h: 28px; } body { @@ -31,6 +34,11 @@ body { margin: 0; } +.page-body { + padding: 0 16px; + box-sizing: border-box; +} + /* ── Game grid ── */ #actors { @@ -40,22 +48,19 @@ body { } #actors td { - width: 38px; - height: 38px; + width: var(--cell); + height: var(--cell); text-align: center; vertical-align: middle; } -#actors td:first-child { - padding-right: 12px; -} .letter-input { - width: 38px; - height: 38px; + width: var(--cell); + height: var(--cell); text-align: center; font-family: 'Inter', sans-serif; - font-size: 15px; + font-size: var(--cell-font); font-weight: 700; border: 2px solid var(--border); border-radius: var(--radius-sm); @@ -80,11 +85,11 @@ body { } .letter-static { - width: 38px; - height: 38px; + width: var(--cell); + height: var(--cell); text-align: center; font-family: 'Inter', sans-serif; - font-size: 15px; + font-size: var(--cell-font); font-weight: 700; text-transform: uppercase; color: var(--text); @@ -103,11 +108,11 @@ body { /* ── Popover ── */ .popover-trigger { - width: 38px; - height: 28px; + width: var(--cell); + height: var(--trigger-h); border-radius: 6px; border: none; - background: #F02287; + background: var(--orange); cursor: pointer; font-size: 14px; font-weight: 600; @@ -120,7 +125,7 @@ body { } .popover-trigger:hover { - background: #d41a76; + background: var(--orange-mid); } .actor-popover { @@ -515,7 +520,8 @@ body { /* ── Game card ── */ .game-container { - max-width: fit-content; + width: fit-content; + max-width: 100%; margin: 56px auto; padding: 24px 32px 32px; background: var(--surface); @@ -525,7 +531,36 @@ body { } .game-container #actors { - margin: 16px auto 0; + margin: 0; +} + +.game-grid-area { + display: flex; + align-items: flex-start; + margin-top: 16px; +} + +.hint-col { + display: flex; + flex-direction: column; + padding-top: 5px; + gap: 5px; + flex-shrink: 0; + padding-right: 12px; +} + +.hint-cell { + height: var(--cell); + display: flex; + align-items: center; +} + +.hint-separator { + height: 12px; +} + +.game-grid-scroll { + overflow-x: auto; } /* ── Game actions ── */ @@ -592,3 +627,29 @@ body { color: var(--orange); background: var(--surface-tint); } + +/* ── Responsive grid ── */ + +@media (max-width: 600px) { + :root { + --cell: 30px; + --cell-font: 12px; + --trigger-h: 22px; + } + + .game-container { + padding: 16px 16px 24px; + } +} + +@media (max-width: 420px) { + :root { + --cell: 24px; + --cell-font: 10px; + --trigger-h: 18px; + } + + #actors { + border-spacing: 3px; + } +} diff --git a/templates/base.html.twig b/templates/base.html.twig index fcf4e8a..8b65f6a 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -17,6 +17,8 @@ {% include '_navbar.html.twig' %} - {% block body %}{% endblock %} +
+ {% block body %}{% endblock %} +