Révision UX : portrait stable, bandeaux, menu, paysage plateau = écran, vibrations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-03 15:05:27 +01:00
co-authored by Claude Opus 5.5
parent 2be5bdcb4f
commit c6649f94c0
20 changed files with 3064 additions and 554 deletions
+695 -136
View File
@@ -51,6 +51,9 @@
/* ------------------------------------------------------------ Couleurs */
/* Thème clair par défaut ; thème sombre selon le système, ou forcé par le
* menu (data-theme sur <html>). Les deux blocs sombres sont identiques
* (vérifié par app/tests/ui.test.ts). */
:root {
--background: #f5efe5;
--surface: #fffcf6;
@@ -117,12 +120,29 @@
--radius-md: 12px;
--radius-pill: 999px;
/* Hauteur de la barre haute : boutons de 44 px et marges. */
/* Mouvement (interaction.md). */
--duration-instant: 90ms;
--duration-fast: 160ms;
--duration-moderate: 240ms;
--easing-standard: cubic-bezier(0.2, 0, 0, 1);
--easing-exit: cubic-bezier(0.4, 0, 1, 1);
/* Voile sous le menu et les panneaux. */
--scrim: rgba(43, 29, 21, 0.5);
/* Hauteurs fixes de l'écran de jeu en portrait : rien ne bouge sous le
* doigt (interaction.md, principe 2). */
--topbar-height: 60px;
--strip-height: 44px;
--infoline-height: 44px;
--prompt-height: 74px;
--actions-height: 76px;
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root {
:root:not([data-theme="light"]) {
--background: #17110d;
--surface: #221913;
--surface-sunken: #2c211a;
@@ -165,29 +185,84 @@
--point-label: var(--point-number);
--shadow-raised: 0 1px 2px rgba(0, 0, 0, 0.4);
--scrim: rgba(0, 0, 0, 0.6);
color-scheme: dark;
}
}
:root[data-theme="dark"] {
--background: #17110d;
--surface: #221913;
--surface-sunken: #2c211a;
--text: #efe5d5;
--muted: #a99886;
--border: #3a2d24;
--border-strong: #7d6b5b;
--accent: #d0694b;
--on-accent: #1a110c;
--accent-soft: #4a2519;
--focus: #e58a6e;
--error: #f08fa6;
--verdict-good: #7ccba4;
--verdict-doubtful: #e0b04f;
--verdict-bad: #f08fa6;
--brand-laiton: #d6a64a;
--frame: #2a1a10;
--bed: #4a3626;
--bar: #24170e;
--tray: #22150d;
--point-a: #3a2519;
--point-b: #7e6142;
--point-number: #cdbfa8;
--checker-player: #ede3cf;
--checker-player-edge: #8e7c64;
--checker-computer: #140d09;
--checker-computer-edge: #a8957d;
--count-player: #2b1d15;
--count-computer: #f3ead8;
--die-player: #ede3cf;
--pip-player: #2b1d15;
--die-computer: #140d09;
--pip-computer: #ede3cf;
--cube: #ede3cf;
--cube-text: #2b1d15;
--highlight: #e07a5a;
--target: rgba(224, 122, 90, 0.38);
--point-label: var(--point-number);
--shadow-raised: 0 1px 2px rgba(0, 0, 0, 0.4);
--scrim: rgba(0, 0, 0, 0.6);
color-scheme: dark;
}
/* -------------------------------------------------------------- Page */
* {
box-sizing: border-box;
}
/* Pas de « tirer pour rafraîchir » ni de rebond qui recharge la page en
* pleine partie ; pas de texte sélectionné par un appui long. */
html {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
overscroll-behavior: none;
}
body {
margin: 0 auto;
max-width: var(--page-width);
padding: 0 var(--space-4) var(--space-6);
padding: 0 var(--space-4);
font-family: var(--font-sans);
font-size: 17px;
line-height: 25px;
color: var(--text);
background: var(--background);
overscroll-behavior: none;
user-select: none;
-webkit-user-select: none;
}
[hidden] {
@@ -205,14 +280,46 @@ a {
outline-offset: 2px;
}
/* L'écran de jeu en portrait : des zones de hauteur fixe, de haut en bas ;
* la barre d'actions reste collée en bas de l'écran. */
.screen {
display: flex;
flex-direction: column;
min-height: 100vh;
min-height: 100dvh;
}
.game {
display: flex;
flex: 1;
flex-direction: column;
}
.spacer {
flex: 1;
}
/* Logo clair ou sombre selon le thème. */
.logo-dark {
display: none;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .logo-light { display: none; }
:root:not([data-theme="light"]) .logo-dark { display: block; }
}
:root[data-theme="dark"] .logo-light { display: none; }
:root[data-theme="dark"] .logo-dark { display: block; }
/* -------------------------------------------------------- Barre haute */
.topbar {
display: flex;
gap: var(--space-2);
flex: none;
gap: var(--space-1);
align-items: center;
min-height: var(--topbar-height);
padding-block: var(--space-2);
height: var(--topbar-height);
}
.topbar .brand {
@@ -224,9 +331,22 @@ a {
}
.topbar .brand img {
display: block;
width: auto;
height: 32px;
max-width: 100%;
height: 28px;
}
/* ------------------------------------------------------- Pictogrammes */
.icon {
width: 24px;
height: 24px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
flex: none;
}
/* ------------------------------------------------------------ Boutons */
@@ -246,6 +366,8 @@ button {
/* Pas de délai ni de zoom sur un double toucher. */
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
transition: background-color var(--duration-instant) var(--easing-standard),
filter var(--duration-instant) var(--easing-standard);
}
button.secondary {
@@ -260,9 +382,11 @@ button.secondary {
}
}
/* Retour visuel au toucher. */
/* Retour visuel au toucher : le bouton descend de 1 px et s'assombrit de
* 6 %, sans ombre. */
button:active:not(:disabled) {
transform: translateY(1px);
filter: brightness(0.94);
}
button:disabled {
@@ -270,70 +394,210 @@ button:disabled {
cursor: default;
}
.topbar button {
/* Pictogramme seul (ou avec son libellé de 13 px dessous, barre haute). */
.icon-button {
display: inline-flex;
flex-direction: column;
flex: none;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
padding: 0 14px;
font-size: 15px;
line-height: 20px;
padding: 0;
color: var(--text);
background: transparent;
border: 0;
border-radius: var(--radius-md);
}
.actions {
.icon-button.labelled {
width: 60px;
height: 52px;
}
.icon-button .label {
font-size: 13px;
line-height: 15px;
font-weight: 500;
}
@media (hover: hover) {
.icon-button:hover:not(:disabled) {
background: var(--surface-sunken);
}
}
/* ------------------------------------------- Bandeaux des joueurs */
/* Un bandeau par camp, au-dessus du plateau pour l'ordinateur, dessous pour
* vous ; celui qui a la main est teinté. */
.strip {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
flex: none;
gap: 10px;
align-items: center;
height: var(--strip-height);
padding: 0 4px 0 12px;
border-radius: var(--radius-md);
}
.actions button {
flex: 1 1 auto;
.strip.turn {
background: var(--accent-soft);
}
/* --------------------------------------------- Messages sous le plateau */
.strip .who {
flex: 1;
min-width: 0;
overflow: hidden;
font-weight: 700;
white-space: nowrap;
text-overflow: ellipsis;
}
/* Compte de points : information secondaire, discrète. */
.pips {
margin: var(--space-2) 0 0;
.strip .lead {
font-size: 15px;
line-height: 20px;
color: var(--muted);
text-align: center;
white-space: nowrap;
}
/* Le compte de points : 20 px gras, chiffres alignés ; il se touche pour
* ouvrir la bulle du glossaire. */
.strip .count {
min-width: 56px;
min-height: 44px;
padding: 0 8px;
font-size: 20px;
line-height: 24px;
font-weight: 700;
font-variant-numeric: tabular-nums lining-nums;
text-align: right;
color: var(--text);
background: transparent;
border: 0;
border-radius: var(--radius-md);
}
.mini {
flex: none;
width: 18px;
height: 18px;
border-radius: 50%;
}
.mini-player {
background: var(--checker-player);
border: 2px solid var(--checker-player-edge);
}
.mini-computer {
background: var(--checker-computer);
border: 2px solid var(--checker-computer-edge);
}
/* La bulle du compte de points, posée sur le plateau, du côté du bandeau
* touché : elle ne déplace rien. */
.board-box {
position: relative;
flex: none;
}
/* Téléphone en portrait : le plateau, héros de l'écran, prend la largeur
* presque entière (8 px de marge au lieu de 16). */
@media (max-width: 40rem) {
html:not(.landscape) .board-box {
margin-inline: calc(var(--space-2) - var(--space-4));
}
}
.bubble {
position: absolute;
left: var(--space-2);
right: var(--space-2);
z-index: 5;
margin: 0;
padding: 10px 14px;
font-size: 15px;
line-height: 20px;
color: var(--text);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-raised);
animation: fade-in var(--duration-fast) var(--easing-standard);
}
.bubble[data-side="computer"] { top: var(--space-2); }
.bubble[data-side="player"] { bottom: var(--space-2); }
.bubble p {
margin: 0 0 var(--space-1);
}
.bubble-term {
font-weight: 700;
}
.link-button {
min-height: 44px;
padding: 0;
color: var(--accent);
background: transparent;
border: 0;
border-radius: 0;
text-decoration: underline;
text-underline-offset: 3px;
}
/* ----------------------------------------------- Sous le plateau */
/* Ligne d'information, toujours réservée : ce qui vient de se passer (coup
* de l'ordinateur, jet d'ouverture, chargement), et « Revoir » au bout. */
.infoline {
display: flex;
flex: none;
gap: var(--space-2);
align-items: center;
height: var(--infoline-height);
padding-left: 12px;
}
/* Ce qui vient de se passer (coup de l'ordinateur, jet d'ouverture) :
* secondaire, mais lisible. */
.info {
min-height: 25px;
margin: var(--space-3) 0 0;
display: -webkit-box;
flex: 1;
margin: 0;
overflow: hidden;
font-size: 15px;
line-height: 20px;
color: var(--muted);
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
}
.info:empty {
margin-top: 0;
.infoline .replay {
color: var(--accent);
}
/* La consigne du moment : ce qui doit ressortir de l'écran. Fond d'accent
* léger quand c'est au joueur, neutre pendant que l'ordinateur réfléchit. */
/* La consigne du moment : ce qui doit ressortir de l'écran. Hauteur fixe
* (deux lignes) ; fond d'accent léger quand c'est au joueur. */
.prompt {
min-height: calc(25px + 2 * var(--space-3));
margin: var(--space-3) 0;
flex: none;
height: var(--prompt-height);
margin: 0;
padding: var(--space-3) var(--space-4);
overflow: hidden;
font-weight: 700;
background: var(--accent-soft);
border-radius: var(--radius-md);
}
.prompt.thinking,
.prompt.neutral,
body.busy .prompt {
font-weight: 400;
background: var(--surface-sunken);
}
/* Fin de partie : le résultat en titre. */
#game:has(#again:not([hidden])) .prompt {
font-size: 24px;
line-height: 30px;
}
/* L'ordinateur réfléchit : trois points qui apparaissent l'un après
* l'autre (la seule animation en boucle). */
.prompt.thinking::after {
@@ -348,12 +612,288 @@ body.busy .prompt {
75% { content: "..."; }
}
/* Barre d'actions collée en bas : l'action secondaire à gauche, la
* principale à droite, sous le pouce. */
.actions {
position: sticky;
bottom: 0;
z-index: 2;
display: grid;
flex: none;
grid-template-columns: 1fr 1.6fr;
gap: var(--space-2);
height: var(--actions-height);
padding: var(--space-3) 0 var(--space-4);
background: var(--background);
border-top: 1px solid var(--border);
}
.actions .slot {
display: flex;
min-width: 0;
}
.actions .slot button {
flex: 1;
min-width: 0;
padding: 0 12px;
display: inline-flex;
gap: 6px;
align-items: center;
justify-content: center;
}
.actions .slot .icon {
width: 20px;
height: 20px;
}
.move-hint {
align-self: center;
margin: 0;
font-size: 15px;
line-height: 20px;
color: var(--muted);
text-align: center;
}
/* --------------------------------------------------- Fenêtres */
/* Voile sous le menu et les panneaux : il assombrit l'écran derrière. */
.veil {
position: fixed;
inset: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-4);
background: var(--scrim);
animation: fade-in var(--duration-fast) var(--easing-standard);
}
/* Le menu : une feuille qui monte du bas. */
.sheet-veil {
align-items: flex-end;
padding: 0;
}
.sheet {
width: min(100%, var(--page-width));
max-height: 100%;
overflow-y: auto;
padding: var(--space-2) var(--space-4) var(--space-6);
color: var(--text);
background: var(--surface);
border-radius: 20px 20px 0 0;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.2);
animation: sheet-up var(--duration-moderate) var(--easing-standard);
}
.sheet-head {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 52px;
}
.grip {
position: absolute;
top: 0;
left: 50%;
width: 40px;
height: 4px;
margin-left: -20px;
background: var(--border-strong);
border-radius: 2px;
}
.sheet-title {
margin: 0;
font-size: 20px;
line-height: 26px;
}
.sheet .item {
display: flex;
gap: 14px;
align-items: center;
width: 100%;
min-height: 52px;
padding: 0;
font-weight: 500;
text-align: left;
color: var(--text);
background: transparent;
border: 0;
border-bottom: 1px solid var(--border);
border-radius: 0;
}
.sheet .item:last-child {
border-bottom: 0;
}
.sheet .item .icon {
color: var(--muted);
}
.item-value {
margin-left: auto;
font-size: 15px;
color: var(--muted);
}
/* Interrupteur (Vibrations, écran allumé). */
.switch {
position: relative;
flex: none;
width: 44px;
height: 26px;
margin-left: auto;
background: var(--border-strong);
border-radius: 13px;
transition: background-color var(--duration-fast) var(--easing-standard);
}
.switch::after {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
background: var(--surface);
border-radius: 50%;
transition: transform var(--duration-fast) var(--easing-standard);
}
.item[aria-checked="true"] .switch {
background: var(--accent);
}
.item[aria-checked="true"] .switch::after {
background: var(--on-accent);
transform: translateX(18px);
}
/* Panneau centré : confirmation du double, fin de partie, double de
* l'ordinateur en paysage. */
.panel {
width: min(24rem, 100%);
padding: var(--space-4);
color: var(--text);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-raised);
animation: panel-in var(--duration-moderate) var(--easing-standard);
}
.panel p {
margin: 0 0 var(--space-4);
font-size: 20px;
line-height: 26px;
font-weight: 700;
}
.panel-actions {
display: flex;
gap: var(--space-2);
}
.panel-actions button {
flex: 1 1 0;
padding: 0 12px;
}
/* « Nouvelle partie commencée. Reprendre l'ancienne », 10 s, avec sa
* barre de temps. */
.banner {
position: fixed;
left: 50%;
bottom: calc(var(--actions-height) + var(--space-2));
z-index: 9;
display: flex;
gap: var(--space-3);
align-items: center;
width: min(24rem, calc(100% - 2 * var(--space-4)));
padding: var(--space-2) var(--space-3) var(--space-3);
overflow: hidden;
font-size: 15px;
line-height: 20px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-raised);
transform: translateX(-50%);
animation: fade-in var(--duration-fast) var(--easing-standard);
}
.banner span:first-child {
flex: 1;
}
.banner button {
flex: none;
min-height: 44px;
padding: 0 14px;
}
.banner-timer {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 4px;
background: var(--accent);
transform-origin: left;
}
.banner-timer.running {
animation: banner-timer 10s linear forwards;
}
@keyframes banner-timer {
from { transform: scaleX(1); }
to { transform: scaleX(0); }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes sheet-up {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
@keyframes panel-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* Mouvement réduit : tout est instantané, rien ne roule ni ne glisse ; la
* barre de temps reste visible, immobile. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0s !important;
transition-duration: 0s !important;
}
.prompt.thinking::after {
animation: none;
content: "…";
}
.banner-timer.running {
animation: none;
}
button:active:not(:disabled) {
transform: none;
}
@@ -376,16 +916,6 @@ body.busy .prompt {
font-variant-numeric: tabular-nums lining-nums;
}
/* Téléphone en portrait : le plateau, héros de l'écran, prend la largeur
* presque entière (8 px de marge au lieu de 16) ; chaque pixel compte pour
* toucher un pion. */
@media (max-width: 40rem) and (orientation: portrait) {
.board {
width: calc(100% + 2 * (var(--space-4) - var(--space-2)));
margin-inline: calc(var(--space-2) - var(--space-4));
}
}
.board-frame { fill: var(--frame); }
.board-bed { fill: var(--bed); }
.board-bar { fill: var(--bar); }
@@ -393,9 +923,10 @@ body.busy .prompt {
.point-a { fill: var(--point-a); }
.point-b { fill: var(--point-b); }
/* Numéros des flèches : leur taille est une mesure du dessin (attribut
* font-size), calculée pour faire au moins 12 px à l'écran (geometry.ts). */
.point-number {
fill: var(--point-label);
font-size: 13px;
font-weight: 500;
text-anchor: middle;
}
@@ -517,37 +1048,123 @@ body.busy .prompt {
cursor: pointer;
}
/* ------------------------------------------------ Confirmation (videau) */
/* Paysage : numéros sur le cadre (ivoire), comptes de points en pastilles
* aux bouts de la barre, consigne dans la bande, commandes à gauche. */
.point-number.on-frame { fill: var(--point-number); }
/* Une carte au milieu de l'écran, comme le glossaire ; le fond de la page
* s'assombrit derrière. */
.confirm {
width: min(22rem, calc(100vw - 2 * var(--space-4)));
padding: var(--space-4);
color: var(--text);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-raised);
}
.pip-chip { stroke-width: 1.5; }
.chip-player { fill: var(--checker-player); stroke: var(--checker-player-edge); }
.chip-computer { fill: var(--checker-computer); stroke: var(--checker-computer-edge); }
.confirm::backdrop {
background: rgba(43, 29, 21, 0.5);
}
.confirm p {
margin: 0 0 var(--space-4);
.pip-chip-text {
font-weight: 700;
text-anchor: middle;
}
.confirm-actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
.chip-text-player { fill: var(--pip-player); }
.chip-text-computer { fill: var(--pip-computer); }
.band-pill { fill: var(--accent-soft); }
.band-text {
fill: var(--text);
font-weight: 700;
text-anchor: middle;
}
.confirm-actions button {
flex: 1 1 auto;
.control-circle {
fill: none;
stroke: var(--point-number);
stroke-width: 2;
}
.control-icon {
fill: none;
stroke: var(--point-number);
stroke-width: 2px;
stroke-linecap: round;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
}
.control.disabled { opacity: 0.35; }
/* ------------------------------------------------------- Mode paysage */
/* Téléphone tourné (ADR 0012) : le plateau est l'écran, sur la hauteur
* réelle (barre d'adresse comprise), cadre noyer jusqu'aux bords. La
* classe vient de app.ts (rotation du téléphone, ou repli du plein écran). */
html.landscape,
html.landscape body {
height: 100%;
overflow: hidden;
}
html.landscape body {
max-width: none;
padding: 0;
background: var(--frame);
}
html.landscape .topbar,
html.landscape .strip,
html.landscape .infoline,
html.landscape .prompt,
html.landscape .spacer,
html.landscape .actions {
display: none;
}
html.landscape .board-box {
position: fixed;
inset: 0;
}
html.landscape .board {
width: 100%;
height: 100%;
aspect-ratio: auto;
}
html.landscape .banner {
bottom: var(--space-2);
}
/* L'aide reste une page qu'on fait défiler. */
html.landscape.help-open,
html.landscape.help-open body {
height: auto;
overflow: visible;
}
html.landscape.help-open body {
padding: 0 var(--space-4);
background: var(--background);
}
/* Repli du plein écran : le paysage ne peut pas être verrouillé, l'écran
* entier tourne d'un quart de tour dans le portrait. Les touchers suivent
* la rotation ; le plateau et les fenêtres sont posés dans #screen. */
html.rotated .screen {
position: fixed;
top: 0;
left: 0;
width: 100vh;
width: 100dvh;
height: 100vw;
min-height: 0;
overflow: hidden;
transform-origin: top left;
transform: rotate(90deg) translateY(-100%);
}
html.rotated .board-box,
html.rotated .veil {
position: absolute;
}
html.rotated .banner {
position: absolute;
}
/* --------------------------------------------------------------- Aide */
@@ -619,65 +1236,7 @@ body.busy .prompt {
color: var(--muted);
}
/* ------------------------------------------------------- Mode paysage */
/* Téléphone tourné : le plateau prend toute la hauteur de l'écran, à
* gauche ; la barre haute, les messages et les boutons passent dans une
* colonne à sa droite. */
@media (orientation: landscape) and (max-height: 40rem) {
body {
max-width: none;
padding: 0 var(--space-4);
}
/* Le jeu en deux colonnes : #game laisse ses enfants dans la grille de
* la page. Quand l'aide est affichée, #game est caché, la première
* colonne n'a plus de largeur et l'aide prend toute la place. */
body {
display: grid;
grid-template-columns: auto minmax(13rem, 1fr);
grid-template-rows: auto auto auto auto auto 1fr;
column-gap: var(--space-4);
align-items: start;
}
#game {
display: contents;
}
.topbar {
grid-column: 2;
grid-row: 1;
min-height: 0;
padding-block: var(--space-2) 0;
}
.board {
grid-column: 1;
grid-row: 1 / -1;
width: auto;
height: calc(100vh - 2 * var(--space-2));
height: calc(100dvh - 2 * var(--space-2));
max-width: 70vw;
margin-block: var(--space-2);
}
.pips,
.info,
.prompt,
.actions {
grid-column: 2;
}
.pips {
text-align: left;
}
.actions {
margin-bottom: var(--space-2);
}
.help {
grid-column: 1 / -1;
}
.help {
user-select: text;
-webkit-user-select: text;
}