Charte graphique appliquée à l'application, passe d'ergonomie #18

Merged
pierre merged 9 commits from charte-ui into main 2026-11-03 03:56:56 +00:00
19 changed files with 582 additions and 209 deletions
+8 -1
View File
@@ -27,8 +27,15 @@ Les contenus pédagogiques (leçons, textes) sont sous licence CC BY-SA 4.0.
(Juan Pablo del Peral, Huerta Tipográfica), licence
SIL Open Font License 1.1 — https://github.com/google/fonts/tree/main/ofl/alegreyasans
Ses tracés (graisse Bold) composent le logotype, dans `docs/identite/logo/`.
Cinq graisses (Regular, Italic, Medium, Bold, ExtraBold) sont embarquées
dans l'application (`web/fonts/`, avec la licence `OFL.txt`), réduites aux
caractères latins et converties en woff2 par fontTools (143 Ko en tout).
- **Logo et icônes d'Ustagammon** (`docs/identite/logo/`), licence
CC BY-SA 4.0 ; publiés dans `icons/` du site (favicon, icônes de la PWA,
logo de la barre haute).
- **fontTools** (MIT), utilisé par `docs/identite/generer-logo.py` pour
convertir le texte du logotype en tracés.
convertir le texte du logotype en tracés, et pour produire les fichiers
woff2 de la police ; avec **Brotli** (MIT), qui les compresse.
## Outils de développement
+12 -2
View File
@@ -115,9 +115,17 @@ deploy: site ## Sert l'application sur le réseau local (Ctrl+C pour arrêter)
deploy-web: deploy ## Comme make deploy (ancien nom, étape 0b)
# Fichiers de docs/identite/logo/ publiés dans icons/ (manifeste, favicon,
# barre haute).
SITE_ICONS := icone.svg icone-192.png icone-512.png icone-maskable-192.png icone-maskable-512.png \
apple-touch-icon-180.png favicon.svg favicon-32.png \
logo-horizontal-couleur.svg logo-horizontal-sombre.svg symbole-couleur.svg symbole-sombre.svg
# Le site tel qu'il sera publié, reconstruit entièrement à chaque appel :
# - web/ à la racine : la page de l'application, son style, le manifeste, les
# icônes, le Web Worker du moteur, et les pages de test sous moteur/ ;
# - web/ à la racine : la page de l'application, son style, sa police, le
# manifeste, le Web Worker du moteur, et les pages de test sous moteur/ ;
# - sous icons/, le logo et les icônes de la charte, copiés depuis
# docs/identite/logo/ (une seule source, ADR 0010) ;
# - l'application compilée (build/web/src/) sous app/ ;
# - le moteur sous wasm/ ;
# - les copies du glossaire et des scénarios sous docs/, lues par l'aide de
@@ -128,6 +136,8 @@ site: engine-wasm web ## Assemble le site publié dans build/site/
rm -rf $(BUILD_DIR)/site
mkdir -p $(BUILD_DIR)/site/wasm $(BUILD_DIR)/site/docs
cp -r web/. $(BUILD_DIR)/site/
mkdir -p $(BUILD_DIR)/site/icons
cp $(addprefix docs/identite/logo/,$(SITE_ICONS)) $(BUILD_DIR)/site/icons/
cp -r $(BUILD_DIR)/web/src $(BUILD_DIR)/site/app
cp $(BUILD_DIR)/wasm/engine.mjs $(BUILD_DIR)/wasm/engine.wasm $(BUILD_DIR)/wasm/engine.data \
$(BUILD_DIR)/wasm/positions.json $(BUILD_DIR)/site/wasm/
+1
View File
@@ -21,6 +21,7 @@ garde-fous d'architecture (0d.1), déploiement sur https://ustagammon.sans.pub.
| Sous-étape | Contenu | État | Plan |
| --- | --- | --- | --- |
| 0d.2 | Plateau minimal jouable (PWA hors ligne), publié ; reste la vérification avec Pierre sur le Fairphone 4 (`docs/guide-test-etape-0.md`, section « Étape 0d ») | 🔄 | |
| 0d.2+ | Fluidité de la partie : supprimer ce qui interrompt le jeu sans nécessité (bouton « Nouvelle partie » toujours visible, confirmations, étapes inutiles) : demande de Pierre | ⏳ | |
**Critère de fin** : une partie complète jouée sur le Fairphone 4, moins de 2 s
par coup de l'IA en niveau standard, tous les tests au vert.
+18
View File
@@ -47,6 +47,24 @@ python3 -I docs/identite/generer-logo.py build/police/AlegreyaSans-Bold.ttf docs
Les PNG sont des rendus des SVG par Chrome sans interface
(`google-chrome --headless --screenshot`), à la taille indiquée dans leur nom.
## Police de l'application
`web/fonts/` contient Alegreya Sans en cinq graisses, réduite aux caractères
latins (français compris) et convertie en woff2, soit 143 Ko au lieu de
1,3 Mo en TTF. Pour la refaire, depuis les TTF de
https://github.com/google/fonts/tree/main/ofl/alegreyasans (Python 3,
fontTools et brotli, par exemple dans l'image `emscripten/emsdk`) :
```sh
pip install fonttools brotli
for f in Regular Italic Medium Bold ExtraBold; do
python3 -m fontTools.subset AlegreyaSans-$f.ttf \
--output-file=web/fonts/AlegreyaSans-$f.woff2 --flavor=woff2 \
--unicodes="U+0020-007E,U+00A0-00FF,U+0152-0153,U+0178,U+02BC,U+02C6,U+02DC,U+2009,U+2011,U+2013-2014,U+2018-201E,U+2022,U+2026,U+202F,U+2039-203A,U+20AC,U+2192,U+2212" \
--layout-features='*' --name-IDs='*' --name-languages='*'
done
```
## Charte graphique
La charte complète (couleurs des deux thèmes, typographie, composants) est dans
-73
View File
@@ -1,73 +0,0 @@
#!/usr/bin/env python3
# Produit les icônes PNG de l'application (web/icons/icon-192.png et
# icon-512.png) à partir des formes de web/icons/icon.svg : un fond de bois
# et deux pions. Icônes provisoires, en attendant l'identité définitive.
#
# Python seul, sans bibliothèque : chaque pixel est échantillonné 4 × 4 fois
# pour adoucir les bords. À relancer seulement si les formes changent, dans
# la chaîne C (Python 3) :
# docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/src" -w /src \
# ustagammon/build-c:2 python3 scripts/make-icons.py
#
# Copyright (C) 2026 les auteurs d'Ustagammon.
# Licence : GPLv3 ou ultérieure (voir LICENSE).
import struct
import zlib
BACKGROUND = (0x6B, 0x44, 0x26)
# Pions dessinés dans l'ordre : (centre x, centre y, rayon, remplissage,
# bord), en unités d'une icône de 512 pixels ; bord de 10 unités.
CHECKERS = [
(300, 300, 120, (0x2B, 0x22, 0x1C), (0x0D, 0x0A, 0x08)),
(212, 212, 120, (0xFB, 0xF6, 0xEA), (0x7D, 0x6A, 0x52)),
]
EDGE = 10
SAMPLES = 4
def color_at(x, y):
"""Couleur d'un point de l'icône, en unités de 512."""
color = BACKGROUND
for cx, cy, r, fill, edge in CHECKERS:
d2 = (x - cx) ** 2 + (y - cy) ** 2
if d2 <= (r + EDGE / 2) ** 2:
color = fill if d2 <= (r - EDGE / 2) ** 2 else edge
return color
def render(size):
scale = 512 / size
rows = []
for py in range(size):
row = bytearray([0]) # filtre PNG « aucun »
for px in range(size):
total = [0, 0, 0]
for sy in range(SAMPLES):
for sx in range(SAMPLES):
x = (px + (sx + 0.5) / SAMPLES) * scale
y = (py + (sy + 0.5) / SAMPLES) * scale
for i, c in enumerate(color_at(x, y)):
total[i] += c
row += bytes(round(t / SAMPLES**2) for t in total)
rows.append(bytes(row))
return b"".join(rows)
def png(size, pixels):
def chunk(kind, data):
body = kind + data
return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body))
header = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0) # RVB, 8 bits
return (
b"\x89PNG\r\n\x1a\n"
+ chunk(b"IHDR", header)
+ chunk(b"IDAT", zlib.compress(pixels, 9))
+ chunk(b"IEND", b"")
)
for size in (192, 512):
with open(f"web/icons/icon-{size}.png", "wb") as f:
f.write(png(size, render(size)))
+4 -3
View File
@@ -29,10 +29,11 @@ for f in "${fichiers[@]}"; do
if grep -q $'\r' "$f"; then
signaler "$f : fins de ligne Windows (CRLF)"
fi
# Copies fidèles de GNU Backgammon (sources et données) : on ne
# reformate pas leurs espaces, pour rester identique à l'original.
# Copies fidèles de GNU Backgammon (sources et données) et de la licence
# de la police : on ne reformate pas leurs espaces, pour rester
# identique à l'original.
case "$f" in
engine/gnubg/* | engine/data/*) continue ;;
engine/gnubg/* | engine/data/* | web/fonts/OFL.txt) continue ;;
esac
if grep -nE '[[:blank:]]+$' "$f" >/dev/null; then
signaler "$f : espaces en fin de ligne (lignes $(grep -nE '[[:blank:]]+$' "$f" | cut -d: -f1 | paste -sd, -))"
+25 -3
View File
@@ -43,8 +43,28 @@ check "app/main.js" "text/javascript"
check "app/ui/app.js" "text/javascript"
check "app/domain/game/game.js" "text/javascript"
check "manifest.webmanifest" "application/manifest\+json"
check "icons/icon-192.png" "image/png"
check "icons/icon-512.png" "image/png"
# Charte graphique (ADR 0010) : icônes et logo copiés de docs/identite/logo/,
# police Alegreya Sans et sa licence.
check "icons/favicon.svg" "image/svg\+xml"
check "icons/favicon-32.png" "image/png"
check "icons/apple-touch-icon-180.png" "image/png"
check "icons/logo-horizontal-couleur.svg" "image/svg\+xml"
check "icons/logo-horizontal-sombre.svg" "image/svg\+xml"
check "icons/symbole-couleur.svg" "image/svg\+xml"
check "icons/symbole-sombre.svg" "image/svg\+xml"
cmp -s <(curl -s "$base/icons/icone-512.png") docs/identite/logo/icone-512.png \
|| report "/icons/icone-512.png diffère de docs/identite/logo/"
for weight in Regular Italic Medium Bold ExtraBold; do
check "fonts/AlegreyaSans-$weight.woff2" "font/woff2"
done
check "fonts/OFL.txt" "text/plain"
# Chaque fichier désigné par la page (icônes, police, style) est servi.
page="$(curl -s "$base/")"
while read -r ref; do
curl -sf -o /dev/null "$base/$ref" || report "index.html : $ref introuvable"
done < <(grep -oE '(href|src|srcset)="[^"]*"' <<<"$page" | sed -E 's/^[a-z]+="(.*)"$/\1/')
grep -qF "url(\"fonts/AlegreyaSans-Regular.woff2\")" <(curl -s "$base/app.css") \
|| report "app.css : police Alegreya Sans non déclarée"
check "sw.js" "text/javascript"
check "engine-worker.mjs" "text/javascript"
check "wasm/engine.mjs" "text/javascript"
@@ -65,6 +85,8 @@ cmp -s <(curl -s "$base/docs/glossaire/videau.md") docs/glossaire/videau.md \
# Le manifeste désigne des icônes servies, et l'application à la racine.
manifest="$(curl -s "$base/manifest.webmanifest")"
grep -q '"start_url": "./"' <<<"$manifest" || report "manifeste : start_url absent"
grep -q '"theme_color": "#4A2E1F"' <<<"$manifest" || report "manifeste : theme_color autre que le noyer"
grep -q '"purpose": "maskable"' <<<"$manifest" || report "manifeste : pas d'icône maskable"
while read -r icon; do
curl -sf -o /dev/null "$base/$icon" || report "manifeste : icône $icon introuvable"
done < <(grep -o '"src": "[^"]*"' <<<"$manifest" | sed 's/"src": "\(.*\)"/\1/')
@@ -73,7 +95,7 @@ done < <(grep -o '"src": "[^"]*"' <<<"$manifest" | sed 's/"src": "\(.*\)"/\1/')
sw="$(curl -s "$base/sw.js")"
grep -qE '^const VERSION = "[0-9a-f]{16}";' <<<"$sw" || report "sw.js : version absente"
for file in ./ index.html app/main.js wasm/engine.data wasm/engine.wasm moteur/ \
docs/glossaire/videau.md version.json; do
docs/glossaire/videau.md version.json fonts/AlegreyaSans-Bold.woff2 icons/favicon.svg; do
grep -qF " \"$file\"," <<<"$sw" || report "sw.js : $file absent de la liste à mettre en cache"
done
listed="$(grep -c '^ "' <<<"$sw")"
+396 -110
View File
@@ -1,81 +1,191 @@
/* Style de l'application. Palette provisoire et sobre (bois, ivoire, un
* accent vert), en attendant l'identité définitive : toutes les couleurs et
* tailles sont des variables, redéfinies pour le thème sombre. */
/* Style de l'application, selon la charte graphique (ADR 0010,
* docs/identite/charte/) : les couleurs, tailles et rayons sont les
* variables de tokens.json, sous le même nom, redéfinies pour le thème
* sombre ; les composants reprennent bundle.css. */
/* ------------------------------------------------------------ Police */
/* Alegreya Sans (SIL Open Font License 1.1, voir fonts/OFL.txt), réduite
* aux caractères latins et convertie en woff2 (voir docs/identite/README.md).
* Embarquée dans le site, donc dans le cache du service worker : elle
* fonctionne hors ligne. */
@font-face {
font-family: "Alegreya Sans";
src: url("fonts/AlegreyaSans-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Alegreya Sans";
src: url("fonts/AlegreyaSans-Italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Alegreya Sans";
src: url("fonts/AlegreyaSans-Medium.woff2") format("woff2");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Alegreya Sans";
src: url("fonts/AlegreyaSans-Bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Alegreya Sans";
src: url("fonts/AlegreyaSans-ExtraBold.woff2") format("woff2");
font-weight: 800;
font-style: normal;
font-display: swap;
}
/* ------------------------------------------------------------ Couleurs */
:root {
--text: #2b211a;
--muted: #6b5d50;
--background: #f7f2e8;
--accent: #2e6b4f;
--background: #f5efe5;
--surface: #fffcf6;
--surface-sunken: #ece3d4;
--text: #2b1d15;
--muted: #6b5c50;
--border: #ded2c0;
--border-strong: #8e7d6a;
--accent: #a8432b;
--on-accent: #ffffff;
--border: #cbbfae;
--error: #a3261b;
--accent-soft: #f1d9cf;
--focus: #a8432b;
--error: #8c1c3a;
--verdict-good: #2f6b4f;
--verdict-doubtful: #7e5a0f;
--verdict-bad: #8c1c3a;
--brand-noyer: #4a2e1f;
--brand-ivoire: #f3ead8;
--brand-laiton: #8f6a1c;
--frame: #6b4426;
--bed: #e6cfa6;
--bar: #5c3a20;
--tray: #57371e;
--point-a: #9a6439;
--point-b: #c79b69;
--point-number: #f3e7d0;
--checker-player: #fbf6ea;
--checker-player-edge: #7d6a52;
--checker-computer: #2b221c;
--checker-computer-edge: #0d0a08;
--count-player: #2b211a;
--count-computer: #fbf6ea;
--die-player: #fbf6ea;
--pip-player: #2b211a;
--die-computer: #2b221c;
--pip-computer: #fbf6ea;
--cube: #fbf6ea;
--cube-text: #2b211a;
--highlight: #2e6b4f;
--target: rgba(46, 107, 79, 0.35);
--last-move: #d9822b;
--frame: #4a2e1f;
--bed: #e9dcc4;
--bar: #3f2619;
--tray: #3a2418;
--point-a: #6b4430;
--point-b: #c9ae88;
--point-number: #f3ead8;
--checker-player: #f7f0e2;
--checker-player-edge: #8c7a62;
--checker-computer: #2e1c12;
--checker-computer-edge: #120a05;
--count-player: #2b1d15;
--count-computer: #f3ead8;
--die-player: #f7f0e2;
--pip-player: #2b1d15;
--die-computer: #2e1c12;
--pip-computer: #f3ead8;
--cube: #f7f0e2;
--cube-text: #2b1d15;
--highlight: #a8432b;
--target: rgba(168, 67, 43, 0.32);
/* Dernier coup joué (marques et traces) : le laiton, rien d'autre. */
--last-move: var(--brand-laiton);
--font-size: 17px;
/* Numéros des flèches. La charte les pose sur le cadre (point-number,
* ivoire) ; notre plateau les pose sur le fond, où l'ivoire ne se lirait
* pas : texte secondaire en thème clair (4,7:1 sur bed), point-number en
* thème sombre (6,3:1). */
--point-label: var(--muted);
--shadow-raised: 0 1px 2px rgba(43, 29, 21, 0.1);
--font-sans: "Alegreya Sans", "Gill Sans", "Segoe UI", system-ui, sans-serif;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-6: 24px;
--space-8: 32px;
--button-height: 48px;
--radius: 8px;
--page-width: 40rem;
--radius-sm: 6px;
--radius-md: 12px;
--radius-pill: 999px;
/* Hauteur de la barre haute : boutons de 44 px et marges. */
--topbar-height: 60px;
}
@media (prefers-color-scheme: dark) {
:root {
--text: #ece4d6;
--muted: #b3a796;
--background: #1b1714;
--accent: #6fc3a0;
--on-accent: #10221a;
--border: #4d443b;
--error: #f08a7e;
--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: #3a2616;
--bed: #6d5136;
--bar: #2f1f12;
--tray: #2c1d11;
--point-a: #4a3120;
--point-b: #8b6943;
--point-number: #d9cbb4;
--checker-player: #efe6d2;
--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: #17110d;
--checker-computer: #140d09;
--checker-computer-edge: #a8957d;
--highlight: #7fd6b0;
--target: rgba(127, 214, 176, 0.35);
--last-move: #f0a050;
--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);
}
}
/* -------------------------------------------------------------- Page */
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
body {
margin: 0 auto;
max-width: var(--page-width);
padding: 0 0.5rem 1rem;
font: var(--font-size) / 1.45 system-ui, sans-serif;
padding: 0 var(--space-4) var(--space-6);
font-family: var(--font-sans);
font-size: 17px;
line-height: 25px;
color: var(--text);
background: var(--background);
}
@@ -84,78 +194,148 @@ body {
display: none !important;
}
a {
color: var(--accent);
text-underline-offset: 3px;
}
/* Anneau de focus clavier, sur tout élément interactif. */
:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 2px;
}
/* -------------------------------------------------------- Barre haute */
.topbar {
display: flex;
gap: 0.5rem;
gap: var(--space-2);
align-items: center;
padding: 0.5rem 0;
min-height: var(--topbar-height);
padding-block: var(--space-2);
}
.topbar h1 {
.topbar .brand {
flex: 1;
display: flex;
align-items: center;
min-width: 0;
margin: 0;
font-size: 1.25rem;
}
.topbar .brand img {
display: block;
width: auto;
height: 32px;
}
/* ------------------------------------------------------------ Boutons */
/* Ronds comme des pions ; un seul bouton principal (brique) par écran,
* les autres en contour. */
button {
min-height: var(--button-height);
padding: 0 1rem;
padding: 0 20px;
font: inherit;
font-weight: 600;
font-weight: 700;
color: var(--on-accent);
background: var(--accent);
border: 1px solid var(--accent);
border-radius: var(--radius);
border: 2px solid var(--accent);
border-radius: var(--radius-pill);
cursor: pointer;
/* Pas de délai ni de zoom sur un double toucher. */
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
button.secondary {
color: var(--text);
background: transparent;
border-color: var(--border);
border-color: var(--border-strong);
}
.topbar button {
min-height: 40px;
padding: 0 0.75rem;
font-size: 0.9rem;
@media (hover: hover) {
button.secondary:hover:not(:disabled) {
background: var(--surface-sunken);
}
}
/* Retour visuel au toucher. */
button:active:not(:disabled) {
transform: translateY(1px);
}
button:disabled {
opacity: 0.45;
cursor: default;
}
.topbar button {
min-height: 44px;
padding: 0 14px;
font-size: 15px;
line-height: 20px;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
gap: var(--space-2);
}
.actions button {
flex: 1;
flex: 1 1 auto;
}
/* --------------------------------------------- Messages sous le plateau */
/* Compte de points : information secondaire, discrète. */
.pips {
margin: 0.25rem 0 0;
font-size: 0.85rem;
margin: var(--space-2) 0 0;
font-size: 15px;
line-height: 20px;
color: var(--muted);
text-align: center;
font-variant-numeric: tabular-nums lining-nums;
}
.info,
.prompt {
min-height: 1.45em;
margin: 0.4rem 0;
}
/* 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;
color: var(--muted);
}
.prompt {
font-weight: 600;
.info:empty {
margin-top: 0;
}
/* L'ordinateur réfléchit : trois points qui apparaissent l'un après l'autre. */
/* 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. */
.prompt {
min-height: calc(25px + 2 * var(--space-3));
margin: var(--space-3) 0;
padding: var(--space-3) var(--space-4);
font-weight: 700;
background: var(--accent-soft);
border-radius: var(--radius-md);
}
.prompt.thinking,
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 {
content: "";
animation: thinking 1.2s steps(4, end) infinite;
@@ -168,6 +348,17 @@ button:disabled {
75% { content: "..."; }
}
@media (prefers-reduced-motion: reduce) {
.prompt.thinking::after {
animation: none;
content: "…";
}
button:active:not(:disabled) {
transform: none;
}
}
/* ------------------------------------------------------------ Plateau */
.board {
@@ -176,7 +367,23 @@ button:disabled {
height: auto;
aspect-ratio: 640 / 520;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
font-family: var(--font-sans);
/* Alegreya Sans dessine par défaut des chiffres elzéviriens (de hauteurs
* variables) : sur le plateau, des chiffres alignés se lisent mieux. */
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); }
@@ -187,8 +394,9 @@ button:disabled {
.point-b { fill: var(--point-b); }
.point-number {
fill: var(--muted);
fill: var(--point-label);
font-size: 13px;
font-weight: 500;
text-anchor: middle;
}
@@ -206,14 +414,24 @@ button:disabled {
stroke: var(--checker-computer-edge);
}
/* Filet intérieur des pions (bundle.css), prêt pour le jour où le plateau
* le dessine. */
.checker-ring {
fill: none;
stroke-width: 1.5;
}
.ring-player { stroke: var(--checker-player-edge); }
.ring-computer { stroke: var(--checker-computer-edge); }
.checker.movable {
stroke: var(--highlight);
stroke-width: 4;
stroke-width: 3;
}
.checker.selected {
stroke: var(--highlight);
stroke-width: 7;
stroke-width: 6;
}
.checker-count {
@@ -228,8 +446,8 @@ button:disabled {
.target-zone { fill: var(--target); }
.last-move { fill: var(--last-move); }
.die-player { fill: var(--die-player); stroke: var(--checker-player-edge); }
.die-computer { fill: var(--die-computer); stroke: var(--checker-computer-edge); }
.die-player { fill: var(--die-player); stroke: var(--checker-player-edge); stroke-width: 1.5; }
.die-computer { fill: var(--die-computer); stroke: var(--checker-computer-edge); stroke-width: 1.5; }
.pip-player { fill: var(--pip-player); }
.pip-computer { fill: var(--pip-computer); }
@@ -238,7 +456,7 @@ button:disabled {
.cube-value {
fill: var(--cube-text);
font-size: 17px;
font-weight: 700;
font-weight: 800;
text-anchor: middle;
}
@@ -249,70 +467,138 @@ button:disabled {
/* --------------------------------------------------------------- Aide */
.help {
max-width: var(--page-width);
margin: 0 auto;
}
.help h2 {
margin-top: 1.5rem;
font-size: 1.2rem;
margin: var(--space-8) 0 var(--space-2);
font-size: 24px;
line-height: 30px;
font-weight: 700;
}
.help h3 {
margin-bottom: 0.25rem;
font-size: 1rem;
margin: var(--space-6) 0 var(--space-1);
font-size: 20px;
line-height: 26px;
font-weight: 700;
}
.help p {
margin: var(--space-2) 0;
}
.steps {
padding-left: 1.25rem;
margin: var(--space-2) 0;
padding-left: var(--space-6);
}
.steps li + li {
margin-top: var(--space-1);
}
.steps li::marker {
color: var(--muted);
}
/* Glossaire : une liste de définitions, sur une carte. */
.glossary {
margin: var(--space-3) 0 0;
padding: var(--space-1) var(--space-4);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-raised);
}
.glossary dt {
margin-top: 0.6rem;
font-weight: 600;
padding-top: var(--space-3);
font-weight: 700;
}
.glossary dd {
margin: 0;
padding-bottom: var(--space-3);
}
.glossary dd + dt {
border-top: 1px solid var(--border);
}
.about {
margin-top: 2rem;
font-size: 0.85rem;
margin-top: var(--space-8);
font-size: 15px;
line-height: 20px;
color: var(--muted);
}
/* Une cible tactile confortable pour le lien. */
.about a {
color: var(--accent);
display: inline-block;
padding-block: 12px;
}
/* ------------------------------------------------------- Mode paysage */
/* Téléphone tourné : le plateau prend toute la hauteur disponible, et les
* messages et boutons passent dans une colonne à sa droite. */
/* 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);
}
.topbar {
padding: 0.25rem 0;
}
#game {
/* 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(12rem, 1fr);
grid-template-rows: auto auto auto 1fr;
column-gap: 1rem;
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-row: 1 / 5;
grid-column: 1;
grid-row: 1 / -1;
width: auto;
/* Hauteur de l'écran moins la barre du haut (boutons de 40 px). */
height: calc(100vh - 3.5rem);
height: calc(100dvh - 3.5rem);
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;
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2013 The Alegreya Sans Project Authors (https://github.com/huertatipografica/Alegreya-Sans)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

-7
View File
@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<!-- Icône provisoire (pas un logo) : deux pions sur du bois. Les PNG sont
produits à partir des mêmes formes par scripts/make-icons.py. -->
<rect width="512" height="512" fill="#6b4426"/>
<circle cx="300" cy="300" r="120" fill="#2b221c" stroke="#0d0a08" stroke-width="10"/>
<circle cx="212" cy="212" r="120" fill="#fbf6ea" stroke="#7d6a52" stroke-width="10"/>
</svg>

Before

Width:  |  Height:  |  Size: 449 B

+18 -4
View File
@@ -4,17 +4,31 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#6b4426">
<meta name="theme-color" content="#4A2E1F">
<title>Ustagammon</title>
<link rel="manifest" href="manifest.webmanifest">
<link rel="icon" href="icons/icon.svg" type="image/svg+xml">
<link rel="icon" href="icons/favicon.svg" type="image/svg+xml">
<link rel="icon" href="icons/favicon-32.png" sizes="32x32" type="image/png">
<link rel="apple-touch-icon" href="icons/apple-touch-icon-180.png">
<link rel="preload" href="fonts/AlegreyaSans-Regular.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="fonts/AlegreyaSans-Bold.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="app.css">
<script type="module" src="app/main.js"></script>
</head>
<body>
<body class="busy">
<!-- Les textes des boutons et des messages viennent de app/src/ui/texts.ts. -->
<header class="topbar">
<h1>Ustagammon</h1>
<!-- Logo de la charte (docs/identite/logo/, copié dans icons/ par make
site) : horizontal, ou symbole seul quand la place manque (téléphone
étroit, colonne du mode paysage) ; version sombre en thème sombre. -->
<h1 class="brand">
<picture>
<source media="(prefers-color-scheme: dark) and (max-width: 24.5rem), (prefers-color-scheme: dark) and (orientation: landscape) and (max-height: 40rem)" srcset="icons/symbole-sombre.svg">
<source media="(max-width: 24.5rem), (orientation: landscape) and (max-height: 40rem)" srcset="icons/symbole-couleur.svg">
<source media="(prefers-color-scheme: dark)" srcset="icons/logo-horizontal-sombre.svg">
<img src="icons/logo-horizontal-couleur.svg" alt="Ustagammon">
</picture>
</h1>
<button id="help-button" type="button" class="secondary"></button>
<button id="new-game" type="button" class="secondary"></button>
</header>
+7 -6
View File
@@ -7,12 +7,13 @@
"scope": "./",
"display": "standalone",
"orientation": "any",
"background_color": "#f7f2e8",
"theme_color": "#6b4426",
"background_color": "#F5EFE5",
"theme_color": "#4A2E1F",
"icons": [
{ "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" },
{ "src": "icons/icon.svg", "sizes": "any", "type": "image/svg+xml" }
{ "src": "icons/icone-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "icons/icone-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
{ "src": "icons/icone-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
{ "src": "icons/icone-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" },
{ "src": "icons/icone.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" }
]
}