From 6a041a938e48f6e018aa42232fe399d2cd3b42b1 Mon Sep 17 00:00:00 2001 From: sucupira Date: Sun, 16 Aug 2026 22:21:29 -0400 Subject: [PATCH] feat: harmonisation charte OKI dark/light, command bar, local-first stats & SEO --- .gitignore | 3 + app/src/app.html | 19 +- app/src/lib/components/CarteRegions.svelte | 36 +- app/src/lib/components/CommandBar.svelte | 465 +++++++++++++++++++++ app/src/lib/components/FlagBar.svelte | 34 ++ app/src/lib/components/ResultPanel.svelte | 21 +- app/src/lib/components/Seo.svelte | 58 +++ app/src/lib/components/StatsModal.svelte | 329 +++++++++++++++ app/src/lib/components/ThemeSwitch.svelte | 95 +++++ app/src/lib/components/WikiFiche.svelte | 95 ++++- app/src/lib/i18n/fr.ts | 38 +- app/src/lib/i18n/gcf.ts | 112 +++-- app/src/lib/stores/history.svelte.ts | 162 +++++++ app/src/lib/styles/jeu.css | 161 +++---- app/src/lib/styles/oki.css | 417 +++++++++++------- app/src/routes/+layout.svelte | 197 ++++++++- app/src/routes/+page.svelte | 164 ++++++-- app/src/routes/defi/[seed]/+page.svelte | 16 + app/src/routes/explorer/+page.svelte | 96 +++-- app/src/routes/jeu/[mode]/+page.svelte | 34 +- docs/architecture.md | 10 + 21 files changed, 2152 insertions(+), 410 deletions(-) create mode 100644 app/src/lib/components/CommandBar.svelte create mode 100644 app/src/lib/components/FlagBar.svelte create mode 100644 app/src/lib/components/StatsModal.svelte create mode 100644 app/src/lib/components/ThemeSwitch.svelte create mode 100644 app/src/lib/stores/history.svelte.ts diff --git a/.gitignore b/.gitignore index 594cffa..1008da1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Paquet YunoHost (dépôt séparé : ORGANISATION-KA-INTERNATIONALE/jwe_ynh) jwe_ynh/ +# Règles et playbooks internes (non versionnés) +rules/ + # Artefacts de debug Kimi CLI kimi-debug-session*.zip diff --git a/app/src/app.html b/app/src/app.html index 34ef314..7f99dd4 100644 --- a/app/src/app.html +++ b/app/src/app.html @@ -3,12 +3,23 @@ - + + + + %sveltekit.head% diff --git a/app/src/lib/components/CarteRegions.svelte b/app/src/lib/components/CarteRegions.svelte index e0afcb0..3ad41a5 100644 --- a/app/src/lib/components/CarteRegions.svelte +++ b/app/src/lib/components/CarteRegions.svelte @@ -267,59 +267,65 @@ height: auto; } - /* — Régions : tokens OKI, aucun bleu « Google » — */ + /* — Régions : tokens OKI — */ .region path { - fill: var(--oki-vert-doux); - stroke: var(--oki-vert-clair); - stroke-width: 0.9; + fill: var(--card-bg); + stroke: var(--line); + stroke-width: 1; + transition: fill var(--dur-tanbou) var(--ease-ka), stroke var(--dur-tanbou) var(--ease-ka); } .region.survolee path { - fill: var(--oki-bleu-doux); - stroke: var(--oki-bleu); + fill: var(--card-bg-hover); + stroke: var(--or-oki); } .region.choisie path { - fill: var(--oki-vert-clair); - stroke: var(--oki-vert-doux); + fill: rgba(253, 184, 19, 0.18); + stroke: var(--or-oki); + stroke-width: 1.5; } .nom-region { - fill: var(--oki-vert); + fill: var(--blanc-creme); font-size: 15px; font-weight: 700; + font-family: var(--font-display); text-anchor: middle; + letter-spacing: 0.02em; + transition: fill var(--dur-tanbou) var(--ease-ka); } .nom-region.choisie { - text-decoration: underline; + fill: var(--or-oki); } - /* — Marqueurs du récap (guess = corail, réel = vert, comme la légende du bilan) — */ + /* — Marqueurs du récap (guess = rouge OKI, réel = vert OKI) — */ .lien-paire { - stroke: var(--oki-encre-doux); + stroke: var(--muted); stroke-width: 2; stroke-dasharray: 6 4; fill: none; } .marqueur circle { - stroke: var(--oki-blanc); + stroke: var(--noir-profond); stroke-width: 2; } .marqueur.guess circle { - fill: var(--oki-corail); + fill: var(--rouge-oki); } .marqueur.reel circle { - fill: var(--oki-vert); + fill: var(--vert-oki); } .marqueur text { fill: #fff; font-size: 10px; font-weight: 800; + font-family: var(--font-display); text-anchor: middle; pointer-events: none; } diff --git a/app/src/lib/components/CommandBar.svelte b/app/src/lib/components/CommandBar.svelte new file mode 100644 index 0000000..b3dbd7d --- /dev/null +++ b/app/src/lib/components/CommandBar.svelte @@ -0,0 +1,465 @@ + + + + +{#if ouvert} + +{/if} + + diff --git a/app/src/lib/components/FlagBar.svelte b/app/src/lib/components/FlagBar.svelte new file mode 100644 index 0000000..1fb173c --- /dev/null +++ b/app/src/lib/components/FlagBar.svelte @@ -0,0 +1,34 @@ + + + + + diff --git a/app/src/lib/components/ResultPanel.svelte b/app/src/lib/components/ResultPanel.svelte index 48cd1e6..776970e 100644 --- a/app/src/lib/components/ResultPanel.svelte +++ b/app/src/lib/components/ResultPanel.svelte @@ -75,7 +75,9 @@ margin: 0; font-size: 1.3rem; font-weight: 700; - color: var(--oki-vert-clair); + font-family: var(--font-display); + text-transform: uppercase; + color: var(--or-oki); } .score { @@ -83,41 +85,44 @@ } .score .valeur { - font-size: 2.6rem; + font-size: 2.8rem; font-weight: 800; - color: var(--oki-vert); + font-family: var(--font-display); + color: var(--or-oki); } .score .unite { font-size: 1rem; - color: var(--oki-encre-doux); + color: var(--muted); margin-left: 0.3rem; } .distance { margin: 0; - color: var(--oki-encre-doux); + color: var(--blanc-creme); + font-weight: 600; } .rayon { margin: 0.1rem 0 0; font-size: 0.82rem; - color: var(--oki-encre-doux); + color: var(--muted); } .credit-photo { margin: 0; font-size: 0.72rem; - color: var(--oki-encre-doux); + color: var(--muted); text-align: center; } .lieu { margin: 0.5rem 0 0; + color: var(--blanc-creme); } .creole { - color: var(--oki-vert-clair); + color: var(--or-oki); font-style: italic; } diff --git a/app/src/lib/components/Seo.svelte b/app/src/lib/components/Seo.svelte index cab9eac..f72eff1 100644 --- a/app/src/lib/components/Seo.svelte +++ b/app/src/lib/components/Seo.svelte @@ -15,6 +15,61 @@ const titreComplet = $derived(`${SITE_NAME} — ${title}`); const urlCanonique = $derived(`${SITE_URL}${path}`); + + const schemaWebapp = $derived( + JSON.stringify({ + '@context': 'https://schema.org', + '@graph': [ + { + '@type': 'WebApplication', + '@id': `${SITE_URL}/#app`, + name: 'JWE', + alternateName: 'JWE - Géolocalisation Souveraine Outre-mer', + url: SITE_URL, + applicationCategory: 'GameApplication', + genre: ['Educational Game', 'Geolocation Game'], + operatingSystem: 'All', + browserRequirements: 'Requires JavaScript and HTML5 Canvas/WebGL', + inLanguage: ['fr', 'gcf'], + description: + 'Jeu de géolocalisation libre et souverain dans l’esprit de GeoGuessr, centré sur la Guadeloupe, la Martinique, la Guyane et La Réunion.', + offers: { + '@type': 'Offer', + price: '0', + priceCurrency: 'EUR' + }, + author: { + '@type': 'Organization', + name: 'ORGANISATION KA INTERNATIONALE', + url: 'https://o-k-i.net' + }, + license: 'https://opensource.org/licenses/MIT' + }, + { + '@type': 'FAQPage', + '@id': `${SITE_URL}/#faq`, + mainEntity: [ + { + '@type': 'Question', + name: 'Qu’est-ce que JWE ?', + acceptedAnswer: { + '@type': 'Answer', + text: 'JWE est un jeu web de géolocalisation éthique et souverain centré sur 4 territoires caribéens et ultramarins : Guadeloupe, Martinique, Guyane et La Réunion. Le joueur devine la commune d’un lieu à partir d’une photo ou d’une vue street 360°, puis accède à une fiche éducative Wikipédia.' + } + }, + { + '@type': 'Question', + name: 'Qu’est-ce que le principe « Pwen blindé » ?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Le « Pwen blindé » (point aveugle) est une mécanique éducative identifiant les lieux réels qui ne possèdent pas encore de fiche Wikipédia ou Wikidata, invitant les utilisateurs et citoyens à documenter leurs propres territoires pour préserver la mémoire et la souveraineté numérique locale.' + } + } + ] + } + ] + }) + ); @@ -38,4 +93,7 @@ {#if image} {/if} + + + {@html ``} diff --git a/app/src/lib/components/StatsModal.svelte b/app/src/lib/components/StatsModal.svelte new file mode 100644 index 0000000..284e47a --- /dev/null +++ b/app/src/lib/components/StatsModal.svelte @@ -0,0 +1,329 @@ + + + + +{#if ouvert} + +{/if} + + diff --git a/app/src/lib/components/ThemeSwitch.svelte b/app/src/lib/components/ThemeSwitch.svelte new file mode 100644 index 0000000..dc64587 --- /dev/null +++ b/app/src/lib/components/ThemeSwitch.svelte @@ -0,0 +1,95 @@ + + + + + diff --git a/app/src/lib/components/WikiFiche.svelte b/app/src/lib/components/WikiFiche.svelte index 6181eea..899039e 100644 --- a/app/src/lib/components/WikiFiche.svelte +++ b/app/src/lib/components/WikiFiche.svelte @@ -110,36 +110,46 @@ diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 9669811..b2bf4dd 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -1,7 +1,7 @@ const fr = { app: { titre: 'JWE', - sousTitre: 'Jeu de géolocalisation — Guadeloupe, Martinique, Guyane, La Réunion' + sousTitre: 'Jeu de géolocalisation souverain — Guadeloupe, Martinique, Guyane, La Réunion' }, regions: { GUADELOUPE: 'Guadeloupe', @@ -110,6 +110,42 @@ const fr = { zoomOut: 'Dézoomer', carteRegionsLabel: 'Carte des quatre régions : Guadeloupe, Martinique, Guyane, La Réunion' }, + theme: { + passerClair: 'Passer au thème clair', + passerSombre: 'Passer au thème sombre' + }, + commandBar: { + titre: 'Palette d’intentions et de commandes', + placeholder: 'Tape une action, un mode, une région (ex. Guadeloupe, Défi, Thème)…', + aucunResultat: 'Aucune action trouvée pour', + catModes: 'Modes de jeu', + catRegions: 'Régions', + catActions: 'Actions rapides', + mesStats: 'Mes statistiques locales', + basculerTheme: 'Changer de thème (Sombre / Clair)', + basculerLangue: 'Changer de langue (FR / Kréyòl)', + raccourci: 'Ouvrir la palette d’intentions (Ctrl+K)' + }, + stats: { + titre: 'Mes statistiques locales', + sousTitre: 'Données stockées uniquement sur ton appareil (100% privé & souverain)', + fermer: 'Fermer la fenêtre des statistiques', + partiesJouees: 'Parties jouées', + meilleurScore: 'Meilleur score', + scoreMoyen: 'Score moyen', + lieuxDecouverts: 'Lieux découverts', + parRegion: 'Précision par territoire', + aucunePartie: 'Aucune manche jouée', + dernieresParties: 'Dernières parties', + exporterJson: 'Exporter mes données (JSON)', + effacerDonnees: 'Effacer l’historique local', + confirmerEffacer: 'Confirmer la suppression' + }, + footer: { + texteFederation: 'Un service libre & souverain opéré par ORGANISATION KA INTERNATIONALE', + mentionLibre: 'Code sous licence MIT · Données libres OpenStreetMap & Wikipédia', + donner: 'Soutenir sur Liberapay' + }, micro: { bravo: 'Bravo !', kiteSaYe: 'Kité sa ye ?' diff --git a/app/src/lib/i18n/gcf.ts b/app/src/lib/i18n/gcf.ts index 2808bc6..e923706 100644 --- a/app/src/lib/i18n/gcf.ts +++ b/app/src/lib/i18n/gcf.ts @@ -10,82 +10,82 @@ const gcf: Dict = { MARTINIQUE: 'Matinik', GUYANE: 'Giyan', REUNION: 'Léryinyon', - TOUTES: 'Tout péyi yo' + TOUTES: 'Tout sé péyi-la' }, modes: { monument: 'Konnèt moniman', - monumentDesc: 'Roukonèt moniman-an asou foto-a é mété’y asou kat-la.', + monumentDesc: 'Rikonnèt moniman-la asou foto-a é mété-y asou kat-la.', lieu: 'Kote mwen ye ?', - lieuDesc: 'On lari, on bodlanmè, on fowé… chwé kote ou yé.', + lieuDesc: 'On lari, on bò lanmè, on gwo bwa… touvé koté ou yé.', defi: 'Défi 5 rounds', - defiDesc: '5 koté youn dèyè ròt, moniman épi plas mélanjé.', + defiDesc: '5 koté yonn dèyè lòt, moniman éti koté mélanjé.', street: 'An lari a', - streetDesc: 'Mache nan on lari an foto 360° é chwé komin-lan.', + streetDesc: 'Mawonné an lari an 360° é touvé komin-la.', explorer: 'Aprann', - explorerDesc: 'Flanné asou kat-la é li fich Wikipédia yo. San pwen.' + explorerDesc: 'Flanné asou kat-la san pwen, li sé fich Wikipédia-la.' }, accueil: { - choixRegion: 'Chwazi péyi ou', - choixMode: 'Chwazi jan ou vlé jwé', - options: 'Règlaj', - timer: 'Kronomèt (60 s, bonus pwen)', + choixRegion: 'Chwazi péyi a-w', + choixMode: 'Chwazi jwé a-w', + options: 'Chwa', + timer: 'Lidékont (60 s, pwen an plis)', jouer: 'Jwé', - creerLienDefi: 'Réyé on lyen défi' + creerLienDefi: 'Fè on lyen défi' }, jeu: { charger: 'Nou ka chajé koté-la…', - erreur: 'Nou pa rivé chajé on koté. Éséyé ankò.', - reessayer: 'Éséyé ankò', - valider: 'Validé pozisyon mwen', + erreur: 'Pa té pé chajé koté-la. Risiye.', + reessayer: 'Risiye ankò', + valider: 'Validé koté an yé', passer: 'Pasé', indice: 'Endis', indicePenalite: '−10 % pwen', indiceRestant: 'endis ki rété', indicesRestants: 'endis ki rété', - plusDIndice: 'Pa ni endis ankò', - poserMarqueur: 'Kwiké asou kat-la pou mété makò-a', - streetChargement: 'Nou ka chajé vi lari-a…', - streetErreur: 'Nou pa rivé chajé foto Mapillary-la. Éséyé ankò oben pasé o koté swivan.', - streetToken: 'Vi lari-a pa disponib : konfigirasyon Mapillary-la manké.', - streetAriaLabel: 'Vi lari-a. Flèch yo pou avansé, glissé pou gadté toutò.', - round: 'Mannèk', + plusDIndice: 'Pa rété endis ankò', + poserMarqueur: 'Kwiké asou kat-la pou mété makò a-w', + streetChargement: 'Nou ka chajé lari-a…', + streetErreur: 'Pa té pé chajé foto Mapillary-la. Risiye oben pasé a lòt koté-la.', + streetToken: 'Vizyonnèz pa disponib : mank konfigirasyon Mapillary.', + streetAriaLabel: 'Lari entèraktif. Flèch yo pou vansé, glisé pou gadé alantou.', + round: 'Manch', sur: 'asou', difficulte: { FACILE: 'Fasil', MOYEN: 'Mwayen', - DIFFICILE: 'Difisil', - EXPERT: 'Espè' + DIFFICILE: 'Rèd', + EXPERT: 'Mèt-a-manman' } }, resultat: { points: 'pwen', distance: 'Distans', km: 'km', - trouve: 'Ou trouvé ! Aprann pli plis asou koté sa-a.', - aDecouvrir: 'Koté-la té la. Aprann pli plis asou’y.', - lireWiki: 'Li swit-la asou Wikipédia', - rayonScore: 'Réyon pou pwen yo', + trouve: 'Ou touvé ! Vin aprann plis asou koté-lasa.', + aDecouvrir: 'Koté-la té la. Vin aprann plis asou-y.', + lireWiki: 'Li rès-la asou Wikipédia', + rayonScore: 'Riyon pou pwen', commune: 'Komin', region: 'Péyi', - suivant: 'Koté swivan', - voirBilan: 'Wè bilan-an', - rejouer: 'Rèjwé', - retourAccueil: 'Tounen an lakay' + suivant: 'Koté ki ka vin', + voirBilan: 'Gadé rézilta', + rejouer: 'Jwé ankò', + retourAccueil: 'Viré an lakou' }, lacune: { titre: 'Pwen blindé — Sa pa dokimanté', texte: - 'Koté sa-a égzisté, men i pa ni on paj asou Wikipédia ni on fich asou Wikidata. Pou péyi nou, sé on zafè enpòtan anpil : si nou pa dokimanté pwòp téritwa nou, listwa épi jéwografi nou kay disparèt asou kat nimérik épi labaz donné lemonn yo (ki manm ka bay manjé a lintelijans atifisyèl).', + 'Koté-lasa la pou vré, men i pa ni paj asou Wikipédia ni fich asou Wikidata. Pou péyi an nou, sé on gwo zafè : si nou menm pa ka dokimanté tè an nou, listwa é jéyografi an nou ka disparèt adan kat nimerik é bazdédòn oliwon latè (jis entélijans artifisyèl pa ké konnèt nou).', cta: 'Kontribyé sou Wikipédia', - guide: 'Ou bizwen èd pou koumansé ? Swiv gid OKI « Kijan pou kontribyé sou Wikipédia ».', - guideLien: 'Ouvri gid kontribisyon-an' + guide: 'Ou bizwen on koudmen pou koumansé ? Gadé gid OKI « Kijan pou mété zafè asou Wikipédia ».', + guideLien: 'Wouvè gid pou kontribiyé' }, lacuneFr: { - titre: 'Pwen blindé — Pa nyen an fransé', + titre: 'Pa nyen an fransé pou konnyé-la', texte: - 'Fich Wikidata koté sa-a égzisté, mé pésonn pòkò ékri atik Wikipédia-la an fransé. Ou sa tradwi’y oben réyé’y : chak atik ka konté pou téritwa nou égzisté adan ansiklopedi fransé-a.', - cta: 'Tradwi oben réyé atik-la', - ctaWikidata: 'Wè fich Wikidata-la' + 'Fich Wikidata a koté-lasa la, men pèsonn pòkò maké atik Wikipédia an fransé. Ou pé tradwi-y oben kréyé-y : chak atik ni valè pou péyi an nou égzisté adan lansiklopédi-la.', + cta: 'Tradwi oben fè atik-la', + ctaWikidata: 'Gadé fich Wikidata-la' }, defi: { bilan: 'Bilan défi-a', @@ -112,6 +112,42 @@ const gcf: Dict = { zoomOut: 'Dézoumé', carteRegionsLabel: 'Kat 4 péyi yo : Gwadloup, Matinik, Giyan, Léryinyon' }, + theme: { + passerClair: 'Mété tèm klè', + passerSombre: 'Mété tèm nwè' + }, + commandBar: { + titre: 'Palèt lentansyon é komann', + placeholder: 'Mété on aksyon, on mòd, on péyi (egz. Gwadloup, Défi, Tèm)…', + aucunResultat: 'Pa ni ayen pou', + catModes: 'Mòd jwé', + catRegions: 'Péyi yo', + catActions: 'Aksyon vitman', + mesStats: 'Estatistik lokal an mwen', + basculerTheme: 'Chanje tèm (Nwè / Klè)', + basculerLangue: 'Chanje lang (Fransé / Kréyòl)', + raccourci: 'Wouvè palèt-la (Ctrl+K)' + }, + stats: { + titre: 'Estatistik lokal an mwen', + sousTitre: 'Dòn rété anlè zouti a-w sèlman (100% privé é souvren)', + fermer: 'Fèmé fénèt estatistik', + partiesJouees: 'Pati jwé', + meilleurScore: 'Mèyè nòt', + scoreMoyen: 'Nòt mwayen', + lieuxDecouverts: 'Koté ou wè', + parRegion: 'Prézizyon pa péyi', + aucunePartie: 'Pòkò ni manch', + dernieresParties: 'Dènyé pati yo', + exporterJson: 'Èspòté dòn an mwen (JSON)', + effacerDonnees: 'Fasi dòn lokal', + confirmerEffacer: 'Konfirmé siprésyon' + }, + footer: { + texteFederation: 'On sèvis lib é souvren ki fèt pa ORGANISATION KA INTERNATIONALE', + mentionLibre: 'Kòd anba lisans MIT · Dòn lib OpenStreetMap & Wikipédia', + donner: 'Pòté an koudmen asou Liberapay' + }, micro: { bravo: 'Bravo !', kiteSaYe: 'Kité sa ye ?' diff --git a/app/src/lib/stores/history.svelte.ts b/app/src/lib/stores/history.svelte.ts new file mode 100644 index 0000000..8e85d59 --- /dev/null +++ b/app/src/lib/stores/history.svelte.ts @@ -0,0 +1,162 @@ +import type { GameMode, Region } from '$lib/types'; + +export interface RoundRecord { + nom: string; + commune: string; + region: string; + distanceKm: number | null; + score: number; +} + +export interface GameRecord { + id: string; + timestamp: number; + mode: GameMode; + region: Region | 'TOUTES'; + scoreTotal: number; + roundsCount: number; + rounds: RoundRecord[]; +} + +export interface RegionStats { + rounds: number; + scoreTotal: number; + distanceTotalKm: number; + scoreMoyen: number; + distanceMoyenneKm: number; +} + +export interface LocalStats { + totalParties: number; + totalRounds: number; + meilleurScore: number; + scoreMoyen: number; + distanceMoyenneKm: number; + lieuxUniques: number; + parRegion: Record; +} + +const STORAGE_KEY = 'jwe_local_history'; + +let games = $state([]); + +export function initHistory(): void { + if (typeof window === 'undefined') return; + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) { + games = JSON.parse(raw); + } + } catch (e) { + console.warn('Impossible de charger l’historique local', e); + } +} + +function saveHistory(): void { + if (typeof window === 'undefined') return; + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(games)); + } catch (e) { + console.warn('Impossible de sauvegarder l’historique local', e); + } +} + +export function ajouterPartie(record: Omit): void { + const newRecord: GameRecord = { + ...record, + id: typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : String(Date.now()), + timestamp: Date.now() + }; + games = [newRecord, ...games].slice(0, 100); // Conserve les 100 dernières parties + saveHistory(); +} + +export function getHistorique(): GameRecord[] { + return games; +} + +export function getStats(): LocalStats { + const totalParties = games.length; + let totalRounds = 0; + let scoreCumule = 0; + let distanceCumulee = 0; + let distanceCount = 0; + let meilleurScore = 0; + const lieuxSet = new Set(); + + const initRegion = (): RegionStats => ({ + rounds: 0, + scoreTotal: 0, + distanceTotalKm: 0, + scoreMoyen: 0, + distanceMoyenneKm: 0 + }); + + const parRegion: Record = { + GUADELOUPE: initRegion(), + MARTINIQUE: initRegion(), + GUYANE: initRegion(), + REUNION: initRegion() + }; + + for (const g of games) { + if (g.scoreTotal > meilleurScore) meilleurScore = g.scoreTotal; + scoreCumule += g.scoreTotal; + + for (const r of g.rounds) { + totalRounds++; + if (r.distanceKm !== null) { + distanceCumulee += r.distanceKm; + distanceCount++; + } + if (r.nom) lieuxSet.add(r.nom); + + const reg = r.region as Region; + if (parRegion[reg]) { + parRegion[reg].rounds++; + parRegion[reg].scoreTotal += r.score; + if (r.distanceKm !== null) { + parRegion[reg].distanceTotalKm += r.distanceKm; + } + } + } + } + + for (const reg of ['GUADELOUPE', 'MARTINIQUE', 'GUYANE', 'REUNION'] as Region[]) { + const st = parRegion[reg]; + if (st.rounds > 0) { + st.scoreMoyen = Math.round(st.scoreTotal / st.rounds); + st.distanceMoyenneKm = Math.round((st.distanceTotalKm / st.rounds) * 10) / 10; + } + } + + return { + totalParties, + totalRounds, + meilleurScore, + scoreMoyen: totalParties > 0 ? Math.round(scoreCumule / totalParties) : 0, + distanceMoyenneKm: distanceCount > 0 ? Math.round((distanceCumulee / distanceCount) * 10) / 10 : 0, + lieuxUniques: lieuxSet.size, + parRegion + }; +} + +export function exporterHistorique(): void { + if (typeof window === 'undefined') return; + const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(games, null, 2)); + const a = document.createElement('a'); + a.setAttribute('href', dataStr); + a.setAttribute('download', `jwe-historique-${new Date().toISOString().slice(0, 10)}.json`); + document.body.appendChild(a); + a.click(); + a.remove(); +} + +export function reinitialiserHistorique(): void { + games = []; + if (typeof window !== 'undefined') { + try { + localStorage.removeItem(STORAGE_KEY); + } catch {} + } +} diff --git a/app/src/lib/styles/jeu.css b/app/src/lib/styles/jeu.css index 6e2a854..d7d2238 100644 --- a/app/src/lib/styles/jeu.css +++ b/app/src/lib/styles/jeu.css @@ -1,6 +1,7 @@ -/* Styles partagés des écrans de jeu : /jeu/[mode] et /defi/[seed]. - Layout « app » verrouillé au viewport (100dvh via +layout) : la page ne - scrolle jamais, seuls les panneaux internes défilent. Mobile-first. */ +/* ========================================================================== + STYLES DU JEU — /jeu/[mode] et /defi/[seed] + ORGANISATION KA INTERNATIONALE · Layout viewport 100dvh & Dark/Light OKI + ========================================================================== */ .jeu { flex: 1; @@ -18,7 +19,7 @@ gap: 1rem; padding: 1rem; overflow-y: auto; - color: var(--oki-encre-doux); + color: var(--muted); } /* — Split photo | carte — */ @@ -34,17 +35,24 @@ /* Mobile : la photo s'adapte (plafond ~42dvh), la carte garde au minimum 35dvh utiles + la barre de sauts (~3,5rem). */ .panneau-photo { - position: relative; /* ancre du panneau d'indices */ + position: relative; flex: 1 1 0; min-height: 5rem; max-height: 42dvh; + border-radius: var(--radius-md); + overflow: hidden; + background: var(--card-bg); + border: 1px solid var(--line); } .panneau-carte { - position: relative; /* ancre de la puce de statut et de l'aide */ + position: relative; flex: 1 1 0; min-height: calc(35dvh + 3.5rem); display: flex; + border-radius: var(--radius-md); + overflow: hidden; + border: 1px solid var(--line); } .panneau-carte > * { @@ -55,6 +63,8 @@ /* État résultat : le panneau photo devient une fiche scrollable interne. */ .split.resultat .panneau-photo { max-height: none; + border: none; + background: transparent; } .split.resultat .panneau-carte { @@ -64,15 +74,15 @@ .resultat-scroll { height: 100%; overflow-y: auto; - background: var(--oki-blanc); - border-radius: var(--oki-radius); + background: var(--noir-profond); + border: 1px solid var(--line); + border-radius: var(--radius-md); box-shadow: var(--oki-ombre); - padding: 1rem; + padding: 1.2rem; + color: var(--blanc-creme); } -/* — Puce de statut unique (manche, seed, minuteur) : haut-droite, une ligne. - Aucun élément absolu au-dessus des zones interactives : les badges sont - décoratifs, les clics les traversent. — */ +/* — Puce de statut unique (manche, seed, minuteur) : haut-droite, une ligne — */ .statut { position: absolute; top: 0.5rem; @@ -87,23 +97,28 @@ } .badge-manche { - background: rgba(255, 253, 248, 0.94); - color: var(--oki-vert); - border-radius: 999px; + background: var(--noir-profond); + color: var(--or-oki); + border: 1px solid var(--line); + border-radius: var(--radius-sm); padding: 0.25rem 0.8rem; font-size: 0.82rem; font-weight: 700; + font-family: var(--font-display); + letter-spacing: 0.03em; + text-transform: uppercase; white-space: nowrap; box-shadow: var(--oki-ombre); } -/* Seed visible mais discrète : les joueurs vérifient qu'ils jouent le même défi */ +/* Seed visible mais discrète */ .badge-seed { - background: rgba(255, 253, 248, 0.8); - color: var(--oki-encre-doux); - border-radius: 999px; + background: var(--noir-profond); + color: var(--muted); + border: 1px solid var(--line); + border-radius: var(--radius-sm); padding: 0.15rem 0.7rem; - font-size: 0.72rem; + font-size: 0.75rem; font-weight: 600; white-space: nowrap; box-shadow: var(--oki-ombre); @@ -113,8 +128,7 @@ display: flex; } -/* — Indices : panneau scrollable interne, en surimpression de la photo - (zone non interactive) — jamais au-dessus de la carte. — */ +/* — Indices : panneau scrollable en surimpression de la photo — */ .indices { position: absolute; left: 0.4rem; @@ -124,39 +138,40 @@ max-height: calc(100% - 0.8rem); overflow-y: auto; margin: 0; - padding: 0.4rem 0.6rem; - background: rgba(255, 253, 248, 0.96); - border-radius: var(--oki-radius-petit); + padding: 0.5rem 0.7rem; + background: var(--noir-profond); + border: 1px solid var(--line); + border-radius: var(--radius-sm); box-shadow: var(--oki-ombre); } .indice { margin: 0.25rem 0; - background: var(--oki-bleu-doux); - border-left: 4px solid var(--oki-bleu); - border-radius: var(--oki-radius-petit); + background: var(--card-bg); + border-left: 4px solid var(--or-oki); + border-radius: var(--radius-sm); padding: 0.5rem 0.8rem; font-size: 0.95rem; + color: var(--blanc-creme); } -/* — Aide « poser le marqueur » : pastille décorative sur la carte, les clics - la traversent (pointer-events: none). Sur mobile elle flotte au bas du - canevas, juste au-dessus de la barre de sauts (~3,65rem) ; sur desktop elle - est décalée à droite pour ne pas chevaucher les sauts en overlay bas-gauche. — */ +/* — Aide « poser le marqueur » — */ .aide-carte { position: absolute; left: 50%; - bottom: 3.9rem; /* 48px de sauts + espacements, arrondi au-dessus */ + bottom: 3.9rem; transform: translateX(-50%); z-index: 3; margin: 0; - padding: 0.3rem 0.9rem; + padding: 0.35rem 0.9rem; max-width: calc(100% - 5rem); - background: rgba(255, 253, 248, 0.92); - color: var(--oki-encre-doux); - border-radius: 999px; + background: var(--noir-profond); + color: var(--blanc-creme); + border: 1px solid var(--line); + border-radius: var(--radius-sm); box-shadow: var(--oki-ombre); font-size: 0.85rem; + font-weight: 600; text-align: center; white-space: nowrap; overflow: hidden; @@ -164,8 +179,7 @@ pointer-events: none; } -/* — Barre d'actions : en bas du flux (la page ne scrolle pas), opaque, - au-dessus de la carte grâce au z-index explicite. — */ +/* — Barre d'actions — */ .barre-actions { position: sticky; bottom: 0; @@ -175,12 +189,12 @@ justify-content: space-between; gap: 0.6rem; padding: 0.6rem 0.8rem calc(0.6rem + env(safe-area-inset-bottom)); - background: var(--oki-blanc); - border-top: 1px solid var(--oki-sable-fonce); + background: var(--noir-profond); + border-top: 1px solid var(--line); } .barre-actions .btn { - min-height: 48px; /* cible tactile */ + min-height: 48px; } .actions-secondaires { @@ -205,7 +219,7 @@ white-space: nowrap; } -/* Écrans très étroits : tout doit tenir sur une ligne. */ +/* Écrans très étroits */ @media (max-width: 420px) { .actions-secondaires .btn { padding: 0.55rem 0.7rem; @@ -222,7 +236,7 @@ } } -/* Paysage / écrans très bas : on relâche les planchers pour éviter le rognage. */ +/* Paysage */ @media (max-height: 540px) { .panneau-photo { min-height: 3.5rem; @@ -234,7 +248,7 @@ } } -/* Desktop : split 50/50 qui remplit exactement la hauteur disponible. */ +/* Desktop : split 50/50 */ @media (min-width: 768px) { .split { flex-direction: row; @@ -258,7 +272,7 @@ .aide-carte { left: auto; right: 0.8rem; - bottom: 3.4rem; /* au-dessus de la rangée de sauts en overlay bas-gauche */ + bottom: 3.4rem; transform: none; } @@ -268,7 +282,7 @@ } } -/* — Écran bilan du défi : compact, scroll interne en dernier recours. — */ +/* — Écran bilan du défi — */ .bilan { flex: 1; min-height: 0; @@ -286,11 +300,9 @@ margin: 0; text-align: center; font-size: 1.6rem; - color: var(--oki-vert); + color: var(--or-oki); } -/* Filet drapeau du bilan : 3 segments francs aux couleurs OKI qui se - dessinent (scaleX) en cascade syncopée — sobre, pas de confettis. */ .filet-drapeau { display: flex; width: 132px; @@ -306,23 +318,22 @@ } .filet-drapeau .seg-vert { - background: var(--oki-vert); + background: var(--vert-oki); } .filet-drapeau .seg-corail { - background: var(--oki-corail); + background: var(--or-oki); } .filet-drapeau .seg-bleu { - background: var(--oki-bleu); + background: var(--turquoise-caraibes); } -/* Seed rappelée sous le titre du bilan (page /defi/[seed]) */ .seed-discret { margin: 0; text-align: center; font-size: 0.9rem; - color: var(--oki-encre-doux); + color: var(--muted); } .total { @@ -333,19 +344,22 @@ .total .valeur { font-size: 2.4rem; font-weight: 800; - color: var(--oki-vert); + font-family: var(--font-display); + color: var(--or-oki); } .total .unite { margin-left: 0.4rem; - color: var(--oki-encre-doux); + color: var(--muted); } -/* Carte SVG du récap : hauteur plafonnée à 44dvh via la largeur - (viewBox 600×620 → hauteur ≈ largeur × 1,033). */ .bilan-carte { width: min(100%, 42.5dvh, 520px); margin: 0 auto; + border-radius: var(--radius-md); + border: 1px solid var(--line); + background: var(--noir-profond); + overflow: hidden; } .recap { @@ -361,25 +375,28 @@ display: flex; align-items: baseline; gap: 0.6rem; - background: var(--oki-blanc); - border-radius: var(--oki-radius-petit); + background: var(--noir-profond); + border: 1px solid var(--line); + border-radius: var(--radius-sm); box-shadow: var(--oki-ombre); padding: 0.5rem 0.9rem; } .r-nom { font-weight: 700; - color: var(--oki-vert); + color: var(--blanc-creme); } .r-region { flex: 1; - color: var(--oki-encre-doux); + color: var(--muted); font-size: 0.9rem; } .r-score { font-weight: 800; + font-family: var(--font-display); + color: var(--or-oki); } .bilan-actions { @@ -397,18 +414,18 @@ margin: 0; text-align: center; font-size: 0.85rem; - color: var(--oki-encre-doux); + color: var(--muted); } .puce.joueur { - color: var(--oki-corail); + color: var(--rouge-oki); } .puce.reelle { - color: var(--oki-vert); + color: var(--vert-oki); } -/* Bilan desktop : deux colonnes (carte | récap) pour tenir sans scroll. */ +/* Bilan desktop */ @media (min-width: 900px) { .bilan { display: grid; @@ -432,7 +449,6 @@ } } -/* Hauteur limitée : resserre le bilan pour rester sans scroll dès 800px. */ @media (max-height: 800px) { .bilan { gap: 0.55rem; @@ -448,11 +464,8 @@ } } -/* — Motion (playbook §1.7) : transform/opacity uniquement, tout est sous - gate html.js + no-preference — contenu statique complet sans JS ni motion. — */ +/* Motion */ @media (prefers-reduced-motion: no-preference) { - /* Moment révélation : la fiche résultat monte pendant que la carte prend - l'emphase (léger zoom de reprise, --dur-mesure --ease-ka). */ html.js .resultat-scroll { animation: jwe-montee var(--dur-mesure) var(--ease-ka); } @@ -461,14 +474,12 @@ animation: jwe-emphase var(--dur-mesure) var(--ease-ka); } - /* Pastilles de statut (manche, seed, minuteur) : apparition en pop. */ html.js .badge-manche, html.js .badge-seed, html.js .badge-timer { animation: oki-pop var(--dur-mesure) var(--ease-ka) backwards; } - /* Filet drapeau du bilan : segments qui se dessinent 0 / 120 / 300 ms. */ html.js .filet-drapeau .seg { animation: jwe-filet var(--dur-mesure) var(--ease-ka) backwards; } @@ -507,8 +518,10 @@ @keyframes jwe-filet { from { transform: scaleX(0); + opacity: 0; } to { transform: scaleX(1); + opacity: 1; } } diff --git a/app/src/lib/styles/oki.css b/app/src/lib/styles/oki.css index 48d73b2..472abbf 100644 --- a/app/src/lib/styles/oki.css +++ b/app/src/lib/styles/oki.css @@ -1,43 +1,283 @@ -/* Tokens OKI — doctrine « calm technology » : couleurs ancrées dans le territoire, - un seul accent fort (corail) réservé à l'action primaire (Von Restorff). */ +/* ========================================================================== + CHARTE OKI — DESIGN SYSTEM & TOKENS CANONIQUES + ORGANISATION KA INTERNATIONALE · Thème sombre par défaut + thème clair WCAG AA + ========================================================================== */ + :root { - --oki-vert: #0b3d2e; /* vert profond — couleur principale de la marque */ + /* ── Noyau (thème sombre = identité par défaut de la marque OKI) ── */ + --noir-oki: #0d0d0d; /* background principal */ + --noir-profond: #1a0f1a; /* surface / cartes */ + --blanc-creme: #fff8e7; /* foreground texte */ + --line: rgba(255, 255, 255, 0.1); /* filets & bordures — jamais de gris plein */ + --or-oki: #fdb813; /* accent : SEULE couleur d'action */ + --or-clair: #ffe066; /* survol / hover action */ + --rouge-oki: #ff1654; /* signal, alerte, ponctuation */ + --vert-oki: #00d66c; /* succès, validation */ + + /* ── Palette étendue ── */ + --turquoise-caraibes: #00ced1; + --jaune-soleil: #ffd700; + --orange-flamme: #ff6b35; + --violet-nuit: #6b2d5c; + --bleu-ocean: #0077be; + --gris-sombre: #2d1b2e; + + /* ── Sémantique dérivée & surfaces ── */ + --muted: color-mix(in srgb, var(--blanc-creme) 72%, transparent); + --card-bg: rgba(255, 255, 255, 0.04); + --card-bg-hover: rgba(255, 255, 255, 0.07); + --border-card: 1px solid var(--line); + --border-btn: 2px solid var(--or-oki); + + /* ── Formes & Typographie ── */ + --radius-sm: 3px; /* boutons, tags, badges — angles nets */ + --radius-md: 6px; /* cartes, conteneurs */ + --radius-lg: 10px; + --font-display: 'Archivo', 'Arial Black', -apple-system, sans-serif; + --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + + /* ── Alias rétro-compatibles JWE ── */ + --oki-vert: var(--vert-oki); --oki-vert-clair: #17624a; - --oki-vert-doux: #e3efe7; - --oki-sable: #f5efe2; /* fonds */ - --oki-sable-fonce: #e7dcc4; - --oki-bleu: #175e70; /* bleu caraïbe profond */ - --oki-bleu-doux: #d9ecef; - --oki-corail: #e4572e; /* accent — action primaire UNIQUEMENT */ - --oki-corail-fonce: #c4441f; - --oki-encre: #22302a; - --oki-encre-doux: #5a6b62; - --oki-blanc: #fffdf8; - --oki-lacune-fond: #fdf3e3; /* panneau Cas B « Pwen blindé » — teinte distincte */ - --oki-lacune-bord: #d9a441; + --oki-vert-doux: color-mix(in srgb, var(--vert-oki) 16%, var(--noir-profond)); + --oki-sable: var(--noir-oki); + --oki-sable-fonce: var(--noir-profond); + --oki-bleu: var(--turquoise-caraibes); + --oki-bleu-doux: color-mix(in srgb, var(--turquoise-caraibes) 15%, var(--noir-profond)); + --oki-corail: var(--or-oki); + --oki-corail-fonce: var(--or-clair); + --oki-encre: var(--blanc-creme); + --oki-encre-doux: var(--muted); + --oki-blanc: var(--noir-profond); + --oki-lacune-fond: color-mix(in srgb, var(--or-oki) 8%, var(--noir-profond)); + --oki-lacune-bord: var(--or-oki); + --oki-radius: var(--radius-md); + --oki-radius-petit: var(--radius-sm); + --oki-ombre: 0 4px 16px rgba(0, 0, 0, 0.35); + --oki-font: var(--font-body); - --oki-radius: 14px; - --oki-radius-petit: 9px; - --oki-ombre: 0 2px 10px rgba(11, 61, 46, 0.14); - --oki-font: - system-ui, - -apple-system, - 'Segoe UI', - Roboto, - sans-serif; - - /* Cadences gwoka (playbook §1.7) — lues aussi depuis le JS via - getComputedStyle ($lib/motion/tokens.ts : dureeMs). */ + /* ── Cadences Gwoka (motion) ── */ --ease-ka: cubic-bezier(0.22, 1, 0.36, 1); /* temps fort : attaque franche, longue tenue */ --ease-syncope: cubic-bezier(0.65, 0, 0.35, 1); /* contretemps : symétrique, urgent */ --dur-tanbou: 120ms; /* double-croche — micro-interactions, hovers */ - --dur-mesure: 480ms; /* une mesure — entrées, transitions d'état */ - --dur-phrase: 960ms; /* deux mesures — moments solennels (ligne de distance) */ - --dur-compte: 800ms; /* count-up du score — entre mesure et phrase */ + --dur-mesure: 480ms; /* une mesure — entrées, transitions */ + --dur-phrase: 960ms; /* deux mesures — moments solennels */ + --dur-compte: 800ms; /* count-up du score */ } -/* Lecture d'écran : valeur finale non animée qui double un compteur animé - (le span animé porte aria-hidden="true"). */ +/* ── Thème clair — opt-in (html.light-theme), accents assombris WCAG AA (ratio ≥ 4.5:1) ── */ +html.light-theme { + --noir-oki: #fff8e7; /* fond crème clair */ + --blanc-creme: #0d0d0d; /* texte encre noir */ + --noir-profond: #f5f0e8; /* surface carte */ + --gris-sombre: #e8ddd0; + + /* Accents assombris pour contraste AA */ + --or-oki: #b87a00; + --or-clair: #d99000; + --vert-oki: #006b3d; + --rouge-oki: #a01030; + --turquoise-caraibes: #006b75; + --bleu-ocean: #004b7f; + --jaune-soleil: #b87a00; + --orange-flamme: #c85000; + + --line: rgba(0, 0, 0, 0.12); + --card-bg: rgba(0, 0, 0, 0.03); + --card-bg-hover: rgba(0, 0, 0, 0.06); + --muted: color-mix(in srgb, var(--blanc-creme) 70%, transparent); + + --oki-vert-doux: #e3efe7; + --oki-bleu-doux: #d9ecef; + --oki-lacune-fond: #fdf3e3; + --oki-lacune-bord: #b87a00; + --oki-ombre: 0 4px 16px rgba(13, 13, 13, 0.08); +} + +/* ── Reset & Base ── */ +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + background: var(--noir-oki); + color: var(--blanc-creme); + font-family: var(--font-body); + line-height: 1.5; + transition: background-color var(--dur-mesure) var(--ease-ka), color var(--dur-mesure) var(--ease-ka); +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: var(--font-display); + text-transform: uppercase; + letter-spacing: -0.01em; + margin: 0 0 0.5rem 0; + color: var(--blanc-creme); +} + +button { + font-family: inherit; +} + +/* ── Signature Flag-Bar (bandeau 6px, 4 segments francs sans dégradé) ── */ +.flag-bar { + height: 6px; + width: 100%; + background: linear-gradient( + to right, + #0d0d0d 0 25%, + var(--or-oki) 25% 50%, + var(--vert-oki) 50% 75%, + var(--rouge-oki) 75% 100% + ); +} + +html.light-theme .flag-bar { + background: linear-gradient( + to right, + #000000 0 25%, + var(--or-oki) 25% 50%, + var(--vert-oki) 50% 75%, + var(--rouge-oki) 75% 100% + ); +} + +/* Flag-chip (mini flag-bar de section 56x6px) */ +.flag-chip { + display: inline-block; + width: 56px; + height: 6px; + border-radius: var(--radius-sm); + background: linear-gradient( + to right, + #0d0d0d 0 25%, + var(--or-oki) 25% 50%, + var(--vert-oki) 50% 75%, + var(--rouge-oki) 75% 100% + ); +} + +html.light-theme .flag-chip { + background: linear-gradient( + to right, + #000000 0 25%, + var(--or-oki) 25% 50%, + var(--vert-oki) 50% 75%, + var(--rouge-oki) 75% 100% + ); +} + +/* ── Boutons OKI ── */ +.btn { + position: relative; + isolation: isolate; + overflow: hidden; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + border-radius: var(--radius-sm); + padding: 0.75rem 1.4rem; + font-size: 1rem; + font-weight: 700; + font-family: var(--font-display); + text-transform: uppercase; + letter-spacing: 0.03em; + cursor: pointer; + text-decoration: none; + transition: + background-color var(--dur-tanbou) var(--ease-ka), + color var(--dur-tanbou) var(--ease-ka), + border-color var(--dur-tanbou) var(--ease-ka), + transform var(--dur-tanbou) var(--ease-ka), + box-shadow var(--dur-tanbou) var(--ease-ka); +} + +.btn:focus-visible, +a:focus-visible, +[tabindex]:focus-visible { + outline: 2px solid var(--or-oki); + outline-offset: 2px; +} + +/* Action primaire : l'or OKI porte toute l'action */ +.btn-primaire { + background: var(--or-oki); + color: #0d0d0d; + border: 2px solid var(--or-oki); + box-shadow: var(--oki-ombre); +} + +html.light-theme .btn-primaire { + background: var(--or-oki); + color: #ffffff; + border: 2px solid var(--or-oki); +} + +/* Balayage hover doré */ +.btn-primaire::after { + content: ''; + position: absolute; + inset: 0; + z-index: -1; + background: var(--or-clair); + transform: scaleX(0); + transform-origin: left center; + transition: transform var(--dur-tanbou) var(--ease-syncope); +} + +.btn-primaire:not(:disabled):hover { + transform: translateY(-2px); + border-color: var(--or-clair); +} + +.btn-primaire:not(:disabled):hover::after { + transform: scaleX(1); +} + +.btn-primaire:disabled { + background: var(--card-bg); + color: var(--muted); + border-color: var(--line); + cursor: not-allowed; + box-shadow: none; +} + +/* Action secondaire */ +.btn-secondaire { + background: var(--card-bg); + color: var(--blanc-creme); + border: 1px solid var(--line); +} + +.btn-secondaire:hover { + background: var(--card-bg-hover); + border-color: var(--or-oki); + color: var(--or-oki); + transform: translateY(-1px); +} + +/* Action tertiaire / discret */ +.btn-tertiaire { + background: transparent; + color: var(--blanc-creme); + border: 1px solid var(--line); +} + +.btn-tertiaire:hover { + border-color: var(--blanc-creme); + background: var(--card-bg); +} + +/* ── Accessibilité & Utilitaires ── */ .sr-only { position: absolute; width: 1px; @@ -51,8 +291,7 @@ border: 0; } -/* Pop de marque (pastilles, marqueurs explorer) : scale 0.6 → 1. - Toujours appliqué sous gate html.js + no-preference chez l'appelant. */ +/* Animations de reveal syncopées */ @keyframes oki-pop { from { transform: scale(0.6); @@ -64,13 +303,10 @@ } } -/* États initiaux des reveals : uniquement si JS actif (classe `js` posée par - app.html) ET motion autorisée — sans JS ou en reduced-motion, tout le - contenu est visible (playbook §6). --d = délai syncopé posé dans le markup. */ @media (prefers-reduced-motion: no-preference) { html.js [data-reveal] { opacity: 0; - transform: translateY(28px); + transform: translateY(24px); transition: opacity var(--dur-mesure) var(--ease-ka) var(--d, 0ms), transform var(--dur-mesure) var(--ease-ka) var(--d, 0ms); @@ -82,111 +318,6 @@ } } -* { - box-sizing: border-box; -} - -html, -body { - margin: 0; - padding: 0; -} - -body { - font-family: var(--oki-font); - color: var(--oki-encre); - background: var(--oki-sable); - line-height: 1.5; -} - -button { - font-family: inherit; -} - -/* — Boutons — */ -.btn { - position: relative; - isolation: isolate; /* le balayage ::after reste sous le texte */ - overflow: hidden; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - border: none; - border-radius: var(--oki-radius-petit); - padding: 0.85rem 1.6rem; - font-size: 1.05rem; - font-weight: 650; - cursor: pointer; - text-decoration: none; - transition: - background-color var(--dur-tanbou) var(--ease-ka), - color var(--dur-tanbou) var(--ease-ka), - border-color var(--dur-tanbou) var(--ease-ka), - transform var(--dur-tanbou) var(--ease-ka); -} - -.btn:focus-visible, -a:focus-visible, -[tabindex]:focus-visible { - outline: 3px solid var(--oki-bleu); - outline-offset: 2px; -} - -/* Action primaire : le seul élément fortement coloré de l'écran */ -.btn-primaire { - background: var(--oki-corail); - color: #fff; - box-shadow: var(--oki-ombre); -} - -/* Balayage corail foncé qui glisse depuis la gauche (hover craft, pattern - .btn OKI adapté au DS corail/vert — transform only) */ -.btn-primaire::after { - content: ''; - position: absolute; - inset: 0; - z-index: -1; - background: var(--oki-corail-fonce); - transform: scaleX(0); - transform-origin: left center; - transition: transform var(--dur-tanbou) var(--ease-syncope); -} - -.btn-primaire:not(:disabled):hover { - transform: translateY(-2px); -} - -.btn-primaire:not(:disabled):hover::after { - transform: scaleX(1); -} - -.btn-primaire:disabled { - background: var(--oki-sable-fonce); - color: var(--oki-encre-doux); - cursor: not-allowed; - box-shadow: none; -} - -.btn-secondaire { - background: var(--oki-vert-doux); - color: var(--oki-vert); -} - -.btn-secondaire:hover { - background: #d2e5d9; -} - -.btn-tertiaire { - background: transparent; - color: var(--oki-vert-clair); - border: 1.5px solid var(--oki-vert-clair); -} - -.btn-tertiaire:hover { - background: var(--oki-vert-doux); -} - @media (prefers-reduced-motion: reduce) { *, *::before, diff --git a/app/src/routes/+layout.svelte b/app/src/routes/+layout.svelte index f19a19c..9189f19 100644 --- a/app/src/routes/+layout.svelte +++ b/app/src/routes/+layout.svelte @@ -2,76 +2,215 @@ import { onMount } from 'svelte'; import '$lib/styles/oki.css'; import LangSwitch from '$lib/components/LangSwitch.svelte'; + import ThemeSwitch from '$lib/components/ThemeSwitch.svelte'; + import FlagBar from '$lib/components/FlagBar.svelte'; + import CommandBar from '$lib/components/CommandBar.svelte'; + import StatsModal from '$lib/components/StatsModal.svelte'; import { initLang, t } from '$lib/i18n/store.svelte'; + import { initHistory } from '$lib/stores/history.svelte'; let { children } = $props(); + let commandBarOuvert = $state(false); + let statsOuvert = $state(false); + onMount(() => { initLang(); + initHistory(); }); + + function onGlobalKeydown(e: KeyboardEvent): void { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { + e.preventDefault(); + commandBarOuvert = !commandBarOuvert; + } + } + +
-
-
+ + + +
{@render children()}
+ + (commandBarOuvert = false)} + onouvrirStats={() => (statsOuvert = true)} + /> + + (statsOuvert = false)} />
diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index 26001f8..87c920f 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -2,6 +2,7 @@ import { goto } from '$app/navigation'; import CarteRegions from '$lib/components/CarteRegions.svelte'; import Seo from '$lib/components/Seo.svelte'; + import FlagBar from '$lib/components/FlagBar.svelte'; import { t } from '$lib/i18n/store.svelte'; import { reveal } from '$lib/motion/reveal'; import { syncopatedDelay } from '$lib/motion/tokens'; @@ -40,7 +41,10 @@
-
+
+

{t().app.titre}

@@ -117,17 +121,31 @@ {t().accueil.jouer}
+ +