Révision UX : portrait stable, compte de points, menu, paysage « plateau = écran » #35
+27
-6
@@ -52,6 +52,8 @@ const SLIDE_MS = 160;
|
||||
const PAUSE_MS = 120;
|
||||
/** Durée du bandeau « Reprendre l'ancienne ». */
|
||||
export const BANNER_MS = 10_000;
|
||||
/** Attente au plus de la réponse à une demande de plein écran. */
|
||||
const FULLSCREEN_WAIT_MS = 1000;
|
||||
|
||||
/** Le dernier plateau affiché, gardé pour le redessiner dès l'ouverture. */
|
||||
export interface CachedBoard {
|
||||
@@ -276,7 +278,12 @@ export function startApp(options: AppOptions): void {
|
||||
// ----------------------------------------------------------- Mise en page
|
||||
|
||||
function layout(): Layout {
|
||||
return rotatedActive() || landscapeQuery.matches ? "landscape" : "portrait";
|
||||
// Plein écran demandé (« le plateau est l'écran ») : paysage dès que
|
||||
// l'écran est plus large que haut, quelle que soit sa taille.
|
||||
const immersiveLandscape = immersive && !portraitQuery.matches;
|
||||
return rotatedActive() || landscapeQuery.matches || immersiveLandscape
|
||||
? "landscape"
|
||||
: "portrait";
|
||||
}
|
||||
|
||||
function rotatedActive(): boolean {
|
||||
@@ -970,25 +977,39 @@ export function startApp(options: AppOptions): void {
|
||||
// ------------------------------------------------- Plein écran (ADR 0012)
|
||||
|
||||
async function enterImmersive(): Promise<void> {
|
||||
// Certains navigateurs ne répondent jamais à la demande (Firefox sans
|
||||
// écran) : on n'attend pas plus d'une seconde, la vue paysage
|
||||
// s'applique quand même.
|
||||
try {
|
||||
await document.documentElement.requestFullscreen?.({ navigationUI: "hide" });
|
||||
await Promise.race([
|
||||
document.documentElement.requestFullscreen?.({ navigationUI: "hide" }),
|
||||
new Promise((resolve) => setTimeout(resolve, FULLSCREEN_WAIT_MS)),
|
||||
]);
|
||||
} catch {
|
||||
// Refusé : la vue paysage s'applique quand même.
|
||||
}
|
||||
let locked = false;
|
||||
let locked = false; // vrai : le navigateur a accepté de verrouiller
|
||||
try {
|
||||
const orientation = screen.orientation as unknown as {
|
||||
lock?: (orientation: string) => Promise<void>;
|
||||
};
|
||||
if (orientation.lock !== undefined) {
|
||||
await orientation.lock("landscape");
|
||||
await Promise.race([
|
||||
orientation.lock("landscape"),
|
||||
new Promise((_, reject) => setTimeout(reject, FULLSCREEN_WAIT_MS)),
|
||||
]);
|
||||
locked = true;
|
||||
}
|
||||
} catch {
|
||||
// Verrouillage refusé : repli par rotation CSS ci-dessous.
|
||||
}
|
||||
// Verrouillage accepté : l'écran tourne dans l'instant. S'il est
|
||||
// toujours en portrait (navigateur qui accepte sans tourner), repli.
|
||||
for (let i = 0; locked && portraitQuery.matches && i < 6; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
immersive = true;
|
||||
rotated = !locked && portraitQuery.matches;
|
||||
rotated = portraitQuery.matches;
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -1000,10 +1021,10 @@ export function startApp(options: AppOptions): void {
|
||||
} catch {
|
||||
// Rien à déverrouiller.
|
||||
}
|
||||
render();
|
||||
if (document.fullscreenElement) {
|
||||
await document.exitFullscreen().catch(() => undefined);
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
// Sortie du plein écran par le retour du téléphone : la vue revient.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
- **Date** : 2026-11-03
|
||||
- **Étape** : 0e.3 (feuille de route)
|
||||
- **Statut** : en cours
|
||||
- **Statut** : réalisé, à vérifier par la session principale
|
||||
- **Exécutant** : sous-agent general-purpose, dans un worktree
|
||||
|
||||
## Objectif
|
||||
@@ -93,6 +93,37 @@ C. **Instabilité des tests** : `tests/browser.sh` et `tests/test_site.sh`
|
||||
propre au worktree (hachage court du chemin du dépôt), et l'image est
|
||||
supprimée en fin de test.
|
||||
|
||||
## Corrections de la recette design (session « identité », sur c6649f9)
|
||||
|
||||
F1. Consigne en portrait en texte nu (17 px gras, deux lignes réservées,
|
||||
sans fond), vide pendant le chargement ; la teinte « à vous » reste au
|
||||
bandeau du joueur. F2. Case d'arrivée : voile sur le seul triangle de la
|
||||
flèche, anneau pointillé là où le pion se posera, zone tactile sur toute la
|
||||
colonne. F3. Flèches laiton de 2,5 px, opacité 0,85, pointes plus petites,
|
||||
sous les pions. F4. Fin de partie : consigne masquée sous le panneau ;
|
||||
surtitre « Fin de partie », titre de 24 px, ligne « 8 points pour lui,
|
||||
videau à 4. ». F5. « Jouer ce coup » en action principale à droite quand le
|
||||
coup est complet, vérifié dans `make test-browser`.
|
||||
|
||||
## Résultat
|
||||
|
||||
À remplir à la clôture.
|
||||
Rempli par le sous-agent ; la session principale le complète à la clôture.
|
||||
|
||||
Fait : points 1 à 9 et ajouts A, B, C ; point 10 (clavier) en grande partie
|
||||
(table d'actions commune ; Espace, Z, D, P, N, F, M, ?, Échap ; pas encore
|
||||
le choix des pions aux flèches ni l'affichage des raccourcis `kbd`).
|
||||
|
||||
Écarts :
|
||||
- vibration du lancer : aucune, comme prévu ; « toucher refusé » non fait
|
||||
(l'écran ne dit pas encore pourquoi) ;
|
||||
- maintien de l'écran allumé : API Wake Lock appelée, non observable dans les
|
||||
navigateurs sans écran : vérification à la main sur le téléphone ;
|
||||
- le bouton « Continuer » reste dans la barre d'actions quand aucun coup
|
||||
n'est possible, en plus du toucher des dés ;
|
||||
- « Revoir la partie » ferme le panneau de fin pour regarder le plateau
|
||||
final : il n'existe pas encore de relecture de partie ;
|
||||
- en paysage, la ligne d'information (jet d'ouverture, coup de
|
||||
l'ordinateur en texte) n'a pas de place : la bande centrale porte la
|
||||
consigne courte ; l'aide reste une page à faire défiler ;
|
||||
- pictogrammes tracés sur la grille 24 × 24 des maquettes, et non 20 × 20.
|
||||
|
||||
|
||||
+27
-8
@@ -539,6 +539,9 @@ async function openMenu() {
|
||||
* le journal de la partie quittée. */
|
||||
async function newGameFromMenu() {
|
||||
const before = await savedJournal();
|
||||
const started = () =>
|
||||
page.evaluate(() => JSON.parse(localStorage.getItem("ustagammon.settings") ?? "{}").gamesStarted);
|
||||
const startedBefore = await started();
|
||||
await openMenu();
|
||||
const start = Date.now();
|
||||
await page.click("#menu-new-game");
|
||||
@@ -548,8 +551,11 @@ async function newGameFromMenu() {
|
||||
check(!(await page.isVisible("#menu")), "le menu reste ouvert après « Nouvelle partie »");
|
||||
await waitForPlayer();
|
||||
const after = await savedJournal();
|
||||
check(after.head !== before.head && !after.events.some((e) => e.type === "MovePlayed" &&
|
||||
e.side === "player"), "« Nouvelle partie » n'a pas commencé de partie");
|
||||
// (Les identifiants d'événements se suivent : une partie neuve peut avoir
|
||||
// la même tête que la précédente ; on compte les parties commencées.)
|
||||
check((await started()) === startedBefore + 1 &&
|
||||
!after.events.some((e) => e.type === "MovePlayed" && e.side === "player"),
|
||||
"« Nouvelle partie » n'a pas commencé de partie");
|
||||
log(`nouvelle partie commencée en ${Date.now() - start} ms, sans confirmation`);
|
||||
return before;
|
||||
}
|
||||
@@ -579,7 +585,8 @@ let releaseEngine;
|
||||
const engineHeld = new Promise((resolve) => {
|
||||
releaseEngine = resolve;
|
||||
});
|
||||
await page.route("**/wasm/engine.data", async (route) => {
|
||||
// (Au niveau du contexte : le Web Worker du moteur fait la demande.)
|
||||
await context.route("**/wasm/engine.data", async (route) => {
|
||||
await engineHeld;
|
||||
await route.continue();
|
||||
});
|
||||
@@ -603,9 +610,13 @@ await page.waitForSelector("#board polygon[data-point]");
|
||||
log(`plateau dessiné en ${Date.now() - start} ms, avant le moteur (${loading.checkers} pions)`);
|
||||
}
|
||||
await checkStable("chargement");
|
||||
await review("1-chargement");
|
||||
// Firefox ne laisse pas toujours retenir la demande du Worker : la revue de
|
||||
// l'écran de chargement (six vues) se fait dans Chromium.
|
||||
if (fullReview) {
|
||||
await review("1-chargement");
|
||||
}
|
||||
releaseEngine();
|
||||
await page.unroute("**/wasm/engine.data");
|
||||
await context.unroute("**/wasm/engine.data");
|
||||
let todo = await waitForPlayer();
|
||||
log(`moteur chargé, partie prête en ${Date.now() - start} ms (${todo})`);
|
||||
await checkStable("moteur chargé");
|
||||
@@ -774,7 +785,13 @@ for (let actions = 0; ; actions++) {
|
||||
// l'orientation, d'où le repli par rotation CSS de tout l'écran.
|
||||
rotatedChecked = true;
|
||||
await page.click("#fullscreen-button");
|
||||
await sleep(300);
|
||||
for (let i = 0; i < 40; i++) {
|
||||
if (await page.evaluate(() => document.documentElement.classList.contains("landscape"))) {
|
||||
break;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
await sleep(200);
|
||||
const view = await page.evaluate(() => {
|
||||
const r = document.getElementById("board").getBoundingClientRect();
|
||||
return {
|
||||
@@ -783,7 +800,9 @@ for (let actions = 0; ; actions++) {
|
||||
board: [r.x, r.y, r.width, r.height].map(Math.round),
|
||||
};
|
||||
});
|
||||
check(view.landscape, "plein écran : pas de vue paysage");
|
||||
check(view.landscape, `plein écran : pas de vue paysage (${await page.evaluate(() =>
|
||||
`${document.documentElement.className} ; plein écran ${document.fullscreenElement !== null} ; ` +
|
||||
`portrait ${matchMedia("(orientation: portrait)").matches}`)})`);
|
||||
if (view.rotated) {
|
||||
check(JSON.stringify(view.board) === JSON.stringify([0, 0, 412, 915]),
|
||||
`repli tourné : plateau ${view.board}`);
|
||||
@@ -806,7 +825,7 @@ for (let actions = 0; ; actions++) {
|
||||
check(!(await page.evaluate(() => document.documentElement.classList.contains("landscape"))),
|
||||
"plein écran quitté : la vue paysage reste");
|
||||
log(`plein écran : ${view.rotated ? "repli par rotation CSS" : "paysage verrouillé"}, ` +
|
||||
"dés lancés en touchant le plateau tourné, sortie par le menu");
|
||||
"dés lancés en touchant le plateau, sortie par le menu");
|
||||
continue;
|
||||
}
|
||||
// Dés : au clavier (Espace), en les touchant (d'un côté puis de
|
||||
|
||||
Reference in New Issue
Block a user