Refocus responsive layouts on compact desktop use
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s
This commit is contained in:
parent
96d058293d
commit
0ac1a0974e
27 changed files with 610 additions and 190 deletions
|
|
@ -159,6 +159,8 @@ Regles :
|
||||||
|
|
||||||
Les heroes de la page d'accueil et des pages jeux utilisent le meme style via `.hero` et `.game-hero`.
|
Les heroes de la page d'accueil et des pages jeux utilisent le meme style via `.hero` et `.game-hero`.
|
||||||
|
|
||||||
|
Sur la page d'accueil, le hero conserve deux colonnes en fenêtre large. Jusqu'à `1024px`, le groupe des deux statistiques passe sous le texte et les actions, aligné à gauche. Les deux cartes restent côte à côte tant que leur largeur le permet.
|
||||||
|
|
||||||
Caracteristiques :
|
Caracteristiques :
|
||||||
|
|
||||||
- fond commun nebula sombre ;
|
- fond commun nebula sombre ;
|
||||||
|
|
@ -205,7 +207,7 @@ Style courant :
|
||||||
- l'engrenage Configuration ouvre un panneau pleine largeur sous l'en-tête, nommé par `aria-controls` / `aria-expanded`. Il regroupe conditionnellement Espace, Guide, réglages propres à l'outil et Données (import/export) ; ouvrir l'ajout ferme Configuration et réciproquement ;
|
- l'engrenage Configuration ouvre un panneau pleine largeur sous l'en-tête, nommé par `aria-controls` / `aria-expanded`. Il regroupe conditionnellement Espace, Guide, réglages propres à l'outil et Données (import/export) ; ouvrir l'ajout ferme Configuration et réciproquement ;
|
||||||
- dans Configuration, Espace et Données partagent la première ligne en deux colonnes. Espace affiche directement le sélecteur compact dont le placeholder décrit l'action. Les réglages propres à l'outil et la liaison Guide occupent toute la largeur en dessous ; si Espace est absent, Données utilise toute la première ligne ;
|
- dans Configuration, Espace et Données partagent la première ligne en deux colonnes. Espace affiche directement le sélecteur compact dont le placeholder décrit l'action. Les réglages propres à l'outil et la liaison Guide occupent toute la largeur en dessous ; si Espace est absent, Données utilise toute la première ligne ;
|
||||||
- le panneau Configuration reste une surface intégrée à l'outil : un seul contour discret, des sous-sections sans effet de carte imbriquée, séparées uniquement par des filets légers, avec libellés et contrôles compacts ;
|
- le panneau Configuration reste une surface intégrée à l'outil : un seul contour discret, des sous-sections sans effet de carte imbriquée, séparées uniquement par des filets légers, avec libellés et contrôles compacts ;
|
||||||
- sur mobile, l'en-tête se répartit sur deux lignes compactes et les sections/champs de Configuration passent en une colonne, dans l'ordre Espace, Données puis réglages spécifiques.
|
- en fenêtre compacte, l'en-tête se répartit sur deux lignes et les sections/champs de Configuration passent en une colonne, dans l'ordre Espace, Données puis réglages spécifiques.
|
||||||
|
|
||||||
Le mot visible cote utilisateur est **outil**, pas **module**. Le terme module reste acceptable uniquement dans le code.
|
Le mot visible cote utilisateur est **outil**, pas **module**. Le terme module reste acceptable uniquement dans le code.
|
||||||
|
|
||||||
|
|
@ -339,7 +341,7 @@ Regles UI :
|
||||||
- les actions de nœud utilisent des boutons icônes : ajout, repli, suppression, déplacement ;
|
- les actions de nœud utilisent des boutons icônes : ajout, repli, suppression, déplacement ;
|
||||||
- le bouton de déplacement doit rester clairement visible sur les cards compactes, notamment les nœuds `Texte` et `Checklist` ;
|
- le bouton de déplacement doit rester clairement visible sur les cards compactes, notamment les nœuds `Texte` et `Checklist` ;
|
||||||
- la progression d'un parent doit rester agrégée et lisible sans afficher une formule détaillée dans la carte ;
|
- la progression d'un parent doit rester agrégée et lisible sans afficher une formule détaillée dans la carte ;
|
||||||
- en panneau latéral ou mobile, l'arbre passe en une colonne sans scroll horizontal.
|
- en panneau latéral ou fenêtre compacte, l'arbre passe en une colonne sans scroll horizontal.
|
||||||
|
|
||||||
### Combos
|
### Combos
|
||||||
|
|
||||||
|
|
@ -389,7 +391,7 @@ Regles UI :
|
||||||
- la creation de mémo est masquee par defaut et s'ouvre via le bouton `+` du header d'outil ;
|
- la creation de mémo est masquee par defaut et s'ouvre via le bouton `+` du header d'outil ;
|
||||||
- réorganisation des cartes par poignée de drag & drop compacte ;
|
- réorganisation des cartes par poignée de drag & drop compacte ;
|
||||||
- bande deroulante visible uniquement quand elle est active et qu'au moins un mémo ou une alerte de planificateur existe ; elle affiche tous les mémos dans l'outil, peut intégrer les tâches non effectuées proches du reset depuis un ou tous les planificateurs de la même toolbox, et reste fixée en bas des pages complètes de cette toolbox ainsi que des pages du jeu qui lui est associé ;
|
- bande deroulante visible uniquement quand elle est active et qu'au moins un mémo ou une alerte de planificateur existe ; elle affiche tous les mémos dans l'outil, peut intégrer les tâches non effectuées proches du reset depuis un ou tous les planificateurs de la même toolbox, et reste fixée en bas des pages complètes de cette toolbox ainsi que des pages du jeu qui lui est associé ;
|
||||||
- la bande fixe laisse le contenu verticalement accessible, suit la largeur disponible et remonte au-dessus de la navigation mobile.
|
- la bande fixe laisse le contenu verticalement accessible et suit la largeur disponible entre la sidebar et le bord droit.
|
||||||
|
|
||||||
### Calque De Dessin
|
### Calque De Dessin
|
||||||
|
|
||||||
|
|
@ -456,12 +458,13 @@ Regles :
|
||||||
- la categorie active commence par un mini-hero descriptif, sans compteur decoratif ;
|
- la categorie active commence par un mini-hero descriptif, sans compteur decoratif ;
|
||||||
- chaque fiche commence par le titre, l'icone, le badge de categorie et une description directe de l'outil ;
|
- chaque fiche commence par le titre, l'icone, le badge de categorie et une description directe de l'outil ;
|
||||||
- l'exemple interactif reste visible en permanence et centre horizontalement ;
|
- l'exemple interactif reste visible en permanence et centre horizontalement ;
|
||||||
- les reperes de boutons propres a l'outil sont places a gauche de l'exemple, puis empiles au-dessus sur mobile ;
|
- les reperes de boutons propres a l'outil sont places a gauche de l'exemple, puis empiles au-dessus dans la disposition compacte jusqu'a `1024px` ;
|
||||||
- les reperes sont groupes par categorie de controles de l'outil, avec apercu visuel du bouton et label court ;
|
- les reperes sont groupes par categorie de controles de l'outil, avec apercu visuel du bouton et label court ;
|
||||||
- les descriptions de reperes sont reservees aux comportements non evidents, par exemple un bouton qui agit sur toute la ligne ou une action de drag & drop ;
|
- les descriptions de reperes sont reservees aux comportements non evidents, par exemple un bouton qui agit sur toute la ligne ou une action de drag & drop ;
|
||||||
- les fonctionnalites detaillees et formats d'import texte sont presentes sous l'exemple dans un panneau repliable pour ne pas alourdir la lecture avant manipulation ;
|
- les fonctionnalites detaillees et formats d'import texte sont presentes sous l'exemple dans un panneau repliable pour ne pas alourdir la lecture avant manipulation ;
|
||||||
- les controles propres au catalogue, comme la reinitialisation d'une demo, ne sont pas documentes comme fonctionnalites de l'outil ;
|
- les controles propres au catalogue, comme la reinitialisation d'une demo, ne sont pas documentes comme fonctionnalites de l'outil ;
|
||||||
- sur mobile, le sommaire devient un bloc normal place avant les fiches, sans position sticky.
|
- en fenêtre compacte jusqu'a `1024px`, le sommaire devient un bloc normal place avant les fiches, sans position sticky ;
|
||||||
|
- sur l'accueil, cette même disposition masque l'illustration de la section « Origine du nom » et sa séparation verticale pour privilégier le dialogue.
|
||||||
|
|
||||||
## 11. Boutons
|
## 11. Boutons
|
||||||
|
|
||||||
|
|
@ -590,7 +593,7 @@ Filtres :
|
||||||
- les options directes avec libellé suivent la largeur naturelle de leur contenu ; lorsqu’elles passent en icône seule, elles retrouvent une largeur compacte commune ;
|
- les options directes avec libellé suivent la largeur naturelle de leur contenu ; lorsqu’elles passent en icône seule, elles retrouvent une largeur compacte commune ;
|
||||||
- la recherche des barres horizontales est plafonnée à `260px` afin de ne pas étirer inutilement les filtres ;
|
- la recherche des barres horizontales est plafonnée à `260px` afin de ne pas étirer inutilement les filtres ;
|
||||||
- `nds-active-filters` forme une seconde section compacte dans la barre principale, séparée sur toute la largeur par un trait doré semi-transparent sans ombre ; la section haute conserve les 10 px de padding vertical de la barre, tandis que la section basse se limite à 4 px ; elle affiche un badge fermable par recherche ou critère actif, dont le bouton retire uniquement le filtre correspondant ;
|
- `nds-active-filters` forme une seconde section compacte dans la barre principale, séparée sur toute la largeur par un trait doré semi-transparent sans ombre ; la section haute conserve les 10 px de padding vertical de la barre, tandis que la section basse se limite à 4 px ; elle affiche un badge fermable par recherche ou critère actif, dont le bouton retire uniquement le filtre correspondant ;
|
||||||
- les barres sont sticky sous la topbar sur desktop, reviennent à la ligne et redeviennent statiques sur mobile ;
|
- les barres sont sticky sous la topbar en fenêtre large, reviennent à la ligne et redeviennent statiques en fenêtre compacte ;
|
||||||
- reset avec bouton icône `rubber`.
|
- reset avec bouton icône `rubber`.
|
||||||
|
|
||||||
Tableaux :
|
Tableaux :
|
||||||
|
|
@ -616,7 +619,7 @@ Outil Tableau :
|
||||||
- La barre d'espaces se place entre le hero de la toolbox et les contrôles de disposition, dans la page complète comme dans le drawer lié à un jeu.
|
- La barre d'espaces se place entre le hero de la toolbox et les contrôles de disposition, dans la page complète comme dans le drawer lié à un jeu.
|
||||||
- Quand seul l'espace par défaut existe, son onglet est masqué et le bouton `Nouvel espace` reste aligné à droite. À partir de deux espaces, la liste devient un `tablist` horizontal scrollable et l'ajout prend la forme d'un bouton `+` placé immédiatement après le dernier onglet.
|
- Quand seul l'espace par défaut existe, son onglet est masqué et le bouton `Nouvel espace` reste aligné à droite. À partir de deux espaces, la liste devient un `tablist` horizontal scrollable et l'ajout prend la forme d'un bouton `+` placé immédiatement après le dernier onglet.
|
||||||
- L'espace actif reprend les surfaces et focus des radio-button groups. Les onglets ciblés par un outil affichent un soulignement doré ; les espaces secondaires disposent d'une poignée de réordonnancement distincte avec un retrait à gauche.
|
- L'espace actif reprend les surfaces et focus des radio-button groups. Les onglets ciblés par un outil affichent un soulignement doré ; les espaces secondaires disposent d'une poignée de réordonnancement distincte avec un retrait à gauche.
|
||||||
- Le réordonnancement des onglets reprend l'indicateur d'insertion doré des outils, positionné à gauche ou à droite selon la destination. Les actions de renommage et suppression restent groupées à droite et passent sous les onglets sur mobile.
|
- Le réordonnancement des onglets reprend l'indicateur d'insertion doré des outils, positionné à gauche ou à droite selon la destination. Les actions de renommage et suppression restent groupées à droite et passent sous les onglets en fenêtre étroite.
|
||||||
|
|
||||||
Les toolboxes sont locales et stockees dans le navigateur.
|
Les toolboxes sont locales et stockees dans le navigateur.
|
||||||
|
|
||||||
|
|
@ -656,13 +659,19 @@ Modales :
|
||||||
Drawer toolbox :
|
Drawer toolbox :
|
||||||
|
|
||||||
- le bouton de topbar associe icone et texte visible : « Afficher la toolbox » quand le drawer est ferme, puis « Masquer la toolbox » lorsqu'il est ouvert ;
|
- le bouton de topbar associe icone et texte visible : « Afficher la toolbox » quand le drawer est ferme, puis « Masquer la toolbox » lorsqu'il est ouvert ;
|
||||||
- panneau lateral ancre a droite, ouvert a `700px` et redimensionnable uniquement vers une largeur superieure ;
|
- à partir de `1440px`, le panneau lateral est ancre a droite, ouvert a `700px` et redimensionnable uniquement vers une largeur superieure ;
|
||||||
- le shell reserve toujours `700px` au panneau : toute largeur supplementaire recouvre la partie droite de la page sans la reduire davantage ;
|
- à partir de `1440px`, le shell reserve toujours `700px` au panneau : toute largeur supplementaire recouvre la partie droite de la page sans la reduire davantage ;
|
||||||
- la page reste visible et interactive, sans backdrop bloquant ni fermeture au clic exterieur ;
|
- la page reste visible et interactive, sans backdrop bloquant ni fermeture au clic exterieur ;
|
||||||
- sous `700px`, le panneau occupe `100vw`, ne reserve plus d'espace lateral et masque sa poignee de redimensionnement ;
|
- sous `1440px`, la topbar propose une bascule « Guide / Toolbox » à la taille standard des groupes de boutons ; chaque vue occupe toute la zone de contenu et la poignée de redimensionnement est masquée ;
|
||||||
|
- le guide et la toolbox restent montés après leur première ouverture en mode compact afin de conserver filtres, saisies et positions de défilement ; la vue masquée est retirée du parcours clavier et de l'arbre d'accessibilité ;
|
||||||
|
- la sidebar mesure `72px` jusqu'à `1024px`, puis `248px` jusqu'au passage en panneau latéral ;
|
||||||
- la largeur revient a `700px` a chaque ouverture et n'est pas persistee ;
|
- la largeur revient a `700px` a chaque ouverture et n'est pas persistee ;
|
||||||
- les actions internes ne doivent pas fermer le drawer sauf action explicite.
|
- les actions internes ne doivent pas fermer le drawer sauf action explicite.
|
||||||
|
|
||||||
|
La cible responsive est une fenêtre d'ordinateur de second écran. Les parcours sont validés à partir de `768px`. Il n'existe pas de navigation dédiée au téléphone ; sous la largeur validée, l'application reste accessible sans garantie de confort.
|
||||||
|
|
||||||
|
Quand la zone utile d'une page toolbox passe sous `900px`, les outils sont forcés sur une seule colonne et le sélecteur une/deux colonnes est masqué. La préférence de disposition reste enregistrée et redevient applicable lorsque la largeur le permet.
|
||||||
|
|
||||||
Notifications internes :
|
Notifications internes :
|
||||||
|
|
||||||
- pas d'alerte navigateur ;
|
- pas d'alerte navigateur ;
|
||||||
|
|
@ -882,7 +891,7 @@ conserve un marqueur d'état lorsque la liste est associée et expose `aria-expa
|
||||||
et `aria-controls`. Le bouton NDS
|
et `aria-controls`. Le bouton NDS
|
||||||
« Associer à la page » / « Dissocier » expose `aria-pressed` ; activer une liste
|
« Associer à la page » / « Dissocier » expose `aria-pressed` ; activer une liste
|
||||||
dissocie la précédente. Le switch « Masquer les cochés » est désactivé sur les listes
|
dissocie la précédente. Le switch « Masquer les cochés » est désactivé sur les listes
|
||||||
non associées. Les commandes sont centrées et reviennent à la ligne sur mobile ;
|
non associées. Les commandes sont centrées et reviennent à la ligne en fenêtre étroite ;
|
||||||
le nombre de correspondances figure dessous. Une liste active devenue incompatible
|
le nombre de correspondances figure dessous. Une liste active devenue incompatible
|
||||||
garde ses commandes avec un message explicite.
|
garde ses commandes avec un message explicite.
|
||||||
Un encart compact placé en haut à droite du header du guide expose un sélecteur dès
|
Un encart compact placé en haut à droite du header du guide expose un sélecteur dès
|
||||||
|
|
|
||||||
191
docs/AUDIT_UX_2026-10-05.md
Normal file
191
docs/AUDIT_UX_2026-10-05.md
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
<!-- Rôle : consigne l'audit d'ergonomie, les observations vérifiées et les améliorations proposées. -->
|
||||||
|
# Audit d'ergonomie de Sokko G
|
||||||
|
|
||||||
|
Date : 5 octobre 2026.
|
||||||
|
|
||||||
|
## Diagnostic
|
||||||
|
|
||||||
|
Sokko G possède une identité visuelle cohérente et une vraie utilité : rassembler les informations personnelles d'une session et les consulter à côté de guides de jeu. Ses 17 outils couvrent déjà de nombreux usages. Le principal axe d'amélioration est de rendre le premier usage évident, puis de raccourcir l'accès aux contenus personnels.
|
||||||
|
|
||||||
|
Aujourd'hui, l'interface expose assez tôt ses possibilités d'organisation, ses réglages et sa gestion des données. Elle explique moins directement quelle action choisir pour répondre à un besoin simple : noter une information, préparer une liste, retrouver sa session ou suivre les éléments d'un guide.
|
||||||
|
|
||||||
|
Les problèmes fonctionnels ci-dessous sont observés ou établis dans le code. Les conséquences sur la compréhension et les bénéfices attendus restent des hypothèses ergonomiques à valider avec des utilisateurs.
|
||||||
|
|
||||||
|
## Périmètre et méthode
|
||||||
|
|
||||||
|
- Lecture des routes, contenus JSON, composants de navigation, accueil, catalogue, toolboxes, modales, sauvegardes, intégration des guides et styles responsive.
|
||||||
|
- Parcours Chromium dans un profil temporaire : accueil, liste vide des toolboxes, création, ouverture d'une toolbox, choix d'un outil, catalogue, liste des jeux, accueil Aniimo et ouverture de son panneau toolbox.
|
||||||
|
- Inspection à 1440 × 1000 et 390 × 844, avec captures et mesures de mise en page.
|
||||||
|
- Contrôle ponctuel du focus et de la touche Échap dans « Ajouter un outil ».
|
||||||
|
- Aucune erreur JavaScript remontée par `pageerror` pendant ces parcours. Aucun débordement horizontal du document détecté dans les vues capturées.
|
||||||
|
|
||||||
|
Limites : vérification sur Vite en développement, sans session avec de vrais utilisateurs, sans audit exhaustif d'accessibilité, sans mesure de performance et sans validation complète des 17 outils. Les filtres détaillés des jeux ont été examinés dans le code, pas exercés dans le navigateur. L'observation initiale à 390 px a permis de retirer une interface téléphone qui ne correspondait pas à la cible produit ; la cible retenue est désormais une fenêtre d'ordinateur à partir de 768 px.
|
||||||
|
|
||||||
|
Le mode développement conserve la prise en main sur l'accueil, même après création. En production, le code la masque dès qu'une toolbox existe. L'entrée « Design system » visible pendant l'audit est également propre au développement : ce n'est pas un défaut de navigation en production.
|
||||||
|
|
||||||
|
## Points à conserver
|
||||||
|
|
||||||
|
- Identité Nebula, palette sombre et composants visuels communs : la base est reconnaissable et cohérente.
|
||||||
|
- Navigation principale courte sur ordinateur.
|
||||||
|
- Création d'une toolbox avec un seul champ obligatoire.
|
||||||
|
- Prise en main existante, exemples interactifs et documentation par outil.
|
||||||
|
- Organisation du catalogue en cinq familles : notes et suivi, références, calcul et données, temps et routines, builds et commandes.
|
||||||
|
- Possibilité d'associer une toolbox à un jeu et d'utiliser les résultats d'un guide pour créer une checklist.
|
||||||
|
- Stockage local, exports et sauvegarde automatique déjà prévus.
|
||||||
|
- Confirmations existantes pour retirer un outil ou supprimer une toolbox : il faut les conserver, pas ajouter des confirmations partout.
|
||||||
|
|
||||||
|
## Priorités proposées
|
||||||
|
|
||||||
|
Les efforts sont relatifs, pas des engagements de délai : faible = changement local ; moyen = plusieurs composants ou états ; élevé = nouvelle logique et validation de données.
|
||||||
|
|
||||||
|
| Priorité | Amélioration | Observation ou justification | Effort |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| P1 | Recentrer la navigation sur le second écran | La navigation téléphone ne correspond pas à la cible d'utilisation. | Faible |
|
||||||
|
| P1 | Harmoniser les libellés de navigation | Conserver les mêmes destinations dans la sidebar large et compacte. | Faible |
|
||||||
|
| P1 | Fiabiliser les modales au clavier | Dans l'ajout d'outil, le focus reste derrière la fenêtre et Échap ne la ferme pas. | Moyen |
|
||||||
|
| P1 | Donner une explication à chaque outil | 17 choix sans description ; plusieurs noms tronqués sur ordinateur. | Moyen |
|
||||||
|
| P1 | Faire de la création la première action | L'accueil propose surtout « Voir les toolboxes » ; le bouton de création guidée est plus bas. | Faible à moyen |
|
||||||
|
| P1 | Placer les toolboxes avant les réglages | En fenêtre compacte, sauvegardes et préférences précèdent la première toolbox et la repoussent sous le premier écran. | Moyen |
|
||||||
|
| P2 | Proposer quelques modèles par usage | Une nouvelle toolbox contient surtout un mémo de bienvenue et un lien vers le catalogue. | Moyen |
|
||||||
|
| P2 | Donner une suite à la démonstration | Le catalogue permet d'essayer les outils, sans action directe pour ajouter l'exemple à sa toolbox. | Moyen |
|
||||||
|
| P2 | Rendre visible la création de checklist depuis un guide | L'action est portée par une petite icône de copie. | Faible à moyen |
|
||||||
|
| P2 | Clarifier enregistrement local et sauvegarde externe | « IndexedDB », export, import et budget de stockage occupent beaucoup de place dans le parcours courant. | Moyen |
|
||||||
|
| P2 | Alléger les barres d'actions et notifications | Beaucoup d'icônes simultanées ; durée par défaut des notifications de 120 secondes dans le code. | Moyen |
|
||||||
|
| P3 | Accueil de reprise, recherche et favoris | L'accueil ne propose pas de raccourci vers une toolbox récente ; la liste n'a pas de recherche. | Moyen à élevé |
|
||||||
|
|
||||||
|
## Recommandations détaillées
|
||||||
|
|
||||||
|
### 1. Une navigation stable en fenêtre large ou compacte
|
||||||
|
|
||||||
|
**Constat vérifié.** L'ancienne barre mobile ne correspondait ni au second écran ni à la fenêtre demi-écran visés par Sokko G. Elle a été retirée au profit d'une sidebar compacte conservée à partir de 768 px.
|
||||||
|
|
||||||
|
**Proposition.** Utiliser des destinations stables dans la sidebar et identifier chaque icône par une infobulle au survol et au focus. Garder la création dans les pages concernées.
|
||||||
|
|
||||||
|
**Validation attendue.** De 768 à 1920 px, toutes les destinations sont identifiables et aucun contrôle fixe ne recouvre le contenu. Sous 768 px, l'application reste accessible sans promesse de confort.
|
||||||
|
|
||||||
|
### 2. Montrer immédiatement ce que l'on peut faire
|
||||||
|
|
||||||
|
**Constat.** L'accueil définit une toolbox dans la prise en main, mais son premier bloc utilise une promesse assez générale. Le bouton « Créer ma première toolbox » est placé après les quatre étapes. À 1440 × 1000, il se situe sous la zone initialement visible.
|
||||||
|
|
||||||
|
**Proposition de texte.** « Vos notes, listes et outils de jeu, réunis au même endroit. Créez une toolbox pour préparer votre prochaine session. Vos données restent dans ce navigateur. »
|
||||||
|
|
||||||
|
**Actions proposées.** Mettre « Créer ma première toolbox » dans le premier bloc, puis « Essayer les outils » en action secondaire. Les guides restent accessibles dans la navigation. Après une première utilisation, remplacer ce bloc par « Reprendre ma session » avec les dernières toolboxes ouvertes.
|
||||||
|
|
||||||
|
Réduire les compteurs « 0 toolbox / 17 outils » à une information secondaire. Déplacer l'histoire du nom vers « C'est quoi Sokko G ? », avec un lien discret depuis l'accueil. Conserver les illustrations comme repères visuels.
|
||||||
|
|
||||||
|
### 3. Rendre le choix d'un outil compréhensible
|
||||||
|
|
||||||
|
**Constat vérifié.** La fenêtre d'ajout présente une grille de 17 icônes et noms. « Planificateur de tâches » et « Planificateur d'équipements » sont tous deux tronqués en « Planificateur … ». Dans une fenêtre très étroite, les outils sont empilés en une longue colonne.
|
||||||
|
|
||||||
|
**Proposition.** Réutiliser les cinq familles du catalogue, afficher les noms complets et une phrase d'usage. Ajouter une recherche par nom et intention, par exemple « noter », « loot », « temps ». Mettre trois choix de démarrage en évidence sans masquer les autres.
|
||||||
|
|
||||||
|
| Outil | Description proposée |
|
||||||
|
| --- | --- |
|
||||||
|
| Mémos | Garder une information courte sous les yeux. |
|
||||||
|
| Bloc-notes | Rédiger un guide personnel ou des notes détaillées. |
|
||||||
|
| Checklist | Cocher des objectifs et suivre des quantités. |
|
||||||
|
| Planificateur de tâches | Suivre des tâches quotidiennes ou hebdomadaires. |
|
||||||
|
| Compteurs | Compter des essais, victoires ou ressources. |
|
||||||
|
| Arbre d'objectifs | Décomposer un objectif en étapes et ressources. |
|
||||||
|
|
||||||
|
Ces distinctions sont particulièrement utiles pour les outils dont les usages se recoupent. Un lien « Voir un exemple » peut ouvrir la bonne section du catalogue.
|
||||||
|
|
||||||
|
### 4. Donner un résultat utile dès la création
|
||||||
|
|
||||||
|
**Constat.** La création standard ajoute des Mémos et des Liens. La visite guidée est demandée par le bouton dédié de l'accueil, mais pas par toutes les entrées « Nouvelle toolbox ». Le tutoriel commence par changer l'icône et renommer, avant d'ajouter un outil.
|
||||||
|
|
||||||
|
**Proposition.** Proposer une option facultative de modèle, sans transformer la création en long formulaire :
|
||||||
|
|
||||||
|
- « Notes de session » : bloc-notes et liens.
|
||||||
|
- « Collecte et progression » : checklist et compteurs.
|
||||||
|
- « Routine quotidienne » : planificateur et minuteur.
|
||||||
|
- « Préparer un build » : équipements, notes et références.
|
||||||
|
- « Personnaliser moi-même » : départ libre.
|
||||||
|
|
||||||
|
Guider d'abord vers une action utile : ajouter une note ou cocher un objectif. Montrer ensuite les possibilités de personnalisation. Proposer de relancer l'aide depuis la toolbox.
|
||||||
|
|
||||||
|
Les modèles sont une piste de conception à tester. Leur implémentation doit respecter le stockage et les formats d'export existants.
|
||||||
|
|
||||||
|
### 5. Mettre les contenus personnels au premier plan
|
||||||
|
|
||||||
|
**Constat vérifié.** La page Toolboxes affiche successivement une présentation, cinq actions, les préférences d'alertes et une grande carte d'explication du stockage avant la première toolbox en fenêtre compacte.
|
||||||
|
|
||||||
|
**Proposition.** Afficher un titre court, « Nouvelle toolbox », puis les toolboxes. Déplacer le positionnement des alertes dans les préférences et regrouper les actions de données dans « Sauvegardes ». Conserver un résumé visible et rassurant : « Enregistré sur cet appareil » avec un accès aux détails.
|
||||||
|
|
||||||
|
Ajouter recherche et favoris lorsque la collection grandit. Un tri « Dernière ouverture » nécessite un suivi local dédié ; la date de modification existante n'est pas un historique de consultation.
|
||||||
|
|
||||||
|
### 6. Relier découverte et utilisation
|
||||||
|
|
||||||
|
**Catalogue.** Les exemples interactifs constituent un bon support d'apprentissage. Ajouter « Ajouter cet outil à une toolbox » à proximité de chaque exemple, avec le choix « Vide » ou « Avec cet exemple ». Expliquer localement que les essais de la démo ne sont pas conservés dans les toolboxes.
|
||||||
|
|
||||||
|
**Guides de jeu.** Remplacer ou accompagner l'icône de copie des résultats par « Créer une checklist ». Montrer le nombre d'éléments concernés et la destination. Si aucune toolbox n'est associée, proposer de la choisir ou d'en créer une dans le même parcours. La fonctionnalité existe déjà en partie : l'enjeu est de rendre son accès et ses prérequis visibles.
|
||||||
|
|
||||||
|
**Panneau latéral.** Afficher le nom de la toolbox liée dans le déclencheur ou à proximité. En fenêtre compacte, la bascule « Guide / Toolbox » évite de perdre le contexte dans un panneau couvrant la page.
|
||||||
|
|
||||||
|
### 7. Clarifier la conservation des données
|
||||||
|
|
||||||
|
**Constat.** Le site explique déjà que les données restent locales et propose des exports ainsi qu'une sauvegarde automatique. Le vocabulaire technique et les détails de fonctionnement sont cependant très présents.
|
||||||
|
|
||||||
|
**Proposition.** Distinguer deux informations :
|
||||||
|
|
||||||
|
- « Enregistré dans ce navigateur » : persistance courante des modifications, à afficher après confirmation effective de l'écriture.
|
||||||
|
- « Dernière sauvegarde en fichier : … » : copie récupérable hors de ce navigateur.
|
||||||
|
|
||||||
|
Employer « Télécharger une sauvegarde » et « Restaurer une sauvegarde » aux endroits appropriés, tout en explicitant si l'import ajoute, fusionne ou remplace des données. Garder les détails IndexedDB et quota dans une rubrique secondaire. La jauge de 250 Mio correspond à un budget recommandé, pas au quota réel du navigateur : éviter un affichage qui suggérerait une limite technique ferme.
|
||||||
|
|
||||||
|
Dans la fenêtre de sauvegarde automatique, mettre d'abord l'état, le dossier et l'action suivante ; replier les explications longues sous « Comment ça fonctionne ».
|
||||||
|
|
||||||
|
### 8. Réduire l'effort de lecture pendant une session
|
||||||
|
|
||||||
|
**Constat visuel.** Les en-têtes d'outils cumulent déplacement, défilement, ajout, réglages, suppression et repli. Le mémo de bienvenue est également visible dans des bandes déroulantes. Les cartes, contours et accents occupent une place importante dans la hiérarchie.
|
||||||
|
|
||||||
|
**Proposition.** Garder visibles l'ajout et les actions fréquentes ; regrouper les actions rares dans « Plus ». Ajouter un libellé aux actions essentielles dans les fenêtres compactes. Envisager un mode « Organiser » explicite pour les déplacements et changements de disposition, après test pour préserver l'efficacité des utilisateurs habitués.
|
||||||
|
|
||||||
|
Réserver l'accent le plus fort à l'action principale du bloc. Rendre les bandes de mémos facultatives au démarrage. Réduire la durée des notifications de succès courantes ; conserver les alertes qui demandent une action jusqu'à leur traitement. Les 120 secondes sont un comportement établi dans le code, pas un délai mesuré pendant cet audit.
|
||||||
|
|
||||||
|
### 9. Corriger les interactions clavier des fenêtres
|
||||||
|
|
||||||
|
**Constat vérifié.** À l'ouverture de « Ajouter un outil », le focus reste sur le bouton déclencheur hors de la modale. Échap ne ferme pas la fenêtre. Le composant ne prévoit pas de confinement du focus. Plusieurs autres modales utilisent un fonctionnement similaire, à contrôler individuellement.
|
||||||
|
|
||||||
|
**Proposition.** Standardiser l'ouverture avec focus dans la fenêtre, la navigation Tab à l'intérieur, la fermeture par Échap et le retour au déclencheur. Rendre l'arrière-plan inactif pendant la modale. Compléter la navigation avec l'indication programmatique de la page courante et un accès direct au contenu principal.
|
||||||
|
|
||||||
|
Le contrôle des contrastes, des cibles tactiles et des lecteurs d'écran reste à réaliser ; cet audit ne conclut pas à une conformité globale d'accessibilité.
|
||||||
|
|
||||||
|
## Ordre de réalisation conseillé
|
||||||
|
|
||||||
|
1. Corriger les défauts vérifiés : navigation de second écran, noms tronqués, focus et fermeture des modales.
|
||||||
|
2. Simplifier le premier parcours : bouton de création immédiat, catalogue d'ajout descriptif, contenus personnels avant les réglages.
|
||||||
|
3. Améliorer les passerelles : exemples vers toolbox et résultats de guide vers checklist.
|
||||||
|
4. Tester les évolutions plus structurantes : modèles, accueil de reprise, favoris, mode d'organisation.
|
||||||
|
|
||||||
|
Éviter d'ajouter d'abord de nouveaux outils : les fonctions actuelles offrent déjà suffisamment de matière pour améliorer la découverte et l'usage quotidien.
|
||||||
|
|
||||||
|
## Comment vérifier les progrès
|
||||||
|
|
||||||
|
Organiser une courte séance avec, par exemple, cinq personnes découvrant le site. Ce petit groupe sert à repérer des obstacles, pas à établir des statistiques représentatives. Leur donner les mêmes objectifs avant et après les changements :
|
||||||
|
|
||||||
|
1. Expliquer à quoi sert le site et où les données sont conservées.
|
||||||
|
2. Créer un espace pour une session et y noter une information.
|
||||||
|
3. Choisir entre checklist et tâches récurrentes pour deux besoins distincts.
|
||||||
|
4. Utiliser un guide pour constituer une checklist personnelle.
|
||||||
|
5. Retrouver la toolbox après rechargement.
|
||||||
|
6. Télécharger une sauvegarde et expliquer comment la restaurer.
|
||||||
|
|
||||||
|
Relever la réussite sans aide, les hésitations, les retours arrière et le temps jusqu'au premier contenu utile. Fixer les objectifs chiffrés après cette première mesure. Inclure un parcours au clavier et des fenêtres réelles de 768 et 960 px.
|
||||||
|
|
||||||
|
## Références dans le dépôt
|
||||||
|
|
||||||
|
- [Accueil et différence développement/production](../website/src/pages/HomePage.jsx)
|
||||||
|
- [Prise en main](../website/src/pages/HomeOnboardingSection.jsx)
|
||||||
|
- [Libellés éditoriaux](../website/public/data/site.json)
|
||||||
|
- [Navigation globale](../website/src/components/Shell.jsx)
|
||||||
|
- [Règles responsive des fenêtres compactes](../website/src/styles/_responsive.scss)
|
||||||
|
- [Pages, tutoriel et panneau toolbox](../website/src/features/toolboxes/ToolboxPages.jsx)
|
||||||
|
- [Sélection d'outils](../website/src/features/toolboxes/modules/AddToolControls.jsx)
|
||||||
|
- [Registre des 17 outils et familles](../website/src/features/toolboxes/toolboxModuleRegistry.js)
|
||||||
|
- [Catalogue et exemples](../website/src/pages/LibraryPage.jsx)
|
||||||
|
- [Résultats de guides vers checklist](../website/src/features/games/CopyChecklistItemsButton.jsx)
|
||||||
|
- [Modales et sauvegarde automatique](../website/src/components/ToolboxModals.jsx)
|
||||||
|
- [Durée des notifications](../website/src/main.jsx)
|
||||||
|
|
||||||
|
Le recentrage responsive issu de cet audit est implémenté ; les autres recommandations restent des pistes de travail hors de ce chantier.
|
||||||
|
|
@ -21,10 +21,10 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
||||||
- Si l'outil porte un état de progression, vérifier que l'import texte repart de l'état initial attendu.
|
- Si l'outil porte un état de progression, vérifier que l'import texte repart de l'état initial attendu.
|
||||||
- Vérifier l'affichage dans la page toolbox complète.
|
- Vérifier l'affichage dans la page toolbox complète.
|
||||||
- Vérifier l'affichage dans le panneau latéral.
|
- Vérifier l'affichage dans le panneau latéral.
|
||||||
- Vérifier l'ordre compact du header en une et deux colonnes et sur mobile : déplacement, icône, titre, scroll éventuel, ajout éventuel, Configuration, suppression, chevron. Le panneau Configuration doit prendre toute la largeur, placer Espace et Données sur deux colonnes puis les réglages spécifiques sur toute la ligne, et empiler ces sections dans cet ordre sur mobile.
|
- Vérifier l'ordre compact du header en une et deux colonnes et dans une fenêtre étroite : déplacement, icône, titre, scroll éventuel, ajout éventuel, Configuration, suppression, chevron. Le panneau Configuration doit prendre toute la largeur, placer Espace et Données sur deux colonnes puis les réglages spécifiques sur toute la ligne, et empiler ces sections sous la largeur prévue.
|
||||||
- Vérifier que Configuration et ajout se ferment mutuellement, qu'Échap ferme Configuration et qu'un outil replié peut l'ouvrir sans afficher son contenu métier.
|
- Vérifier que Configuration et ajout se ferment mutuellement, qu'Échap ferme Configuration et qu'un outil replié peut l'ouvrir sans afficher son contenu métier.
|
||||||
- Pour le tutoriel toolbox, vérifier le démarrage unique, la fermeture et le changement de toolbox en cours de visite ; une autre toolbox doit démarrer à la première étape et ne pas consommer les indicateurs de la précédente.
|
- Pour le tutoriel toolbox, vérifier le démarrage unique, la fermeture et le changement de toolbox en cours de visite ; une autre toolbox doit démarrer à la première étape et ne pas consommer les indicateurs de la précédente.
|
||||||
- Pour le Bloc Note, vérifier la sélection, l'ajout, le renommage et la suppression de pages en page complète, panneau latéral et mobile.
|
- Pour le Bloc Note, vérifier la sélection, l'ajout, le renommage et la suppression de pages en page complète, panneau latéral et fenêtre compacte.
|
||||||
- Vérifier qu'un changement de page avec le focus actif remplace immédiatement le contenu de l'éditeur ; une mise à jour externe de la même page attend le blur sans déplacer la sélection pendant la saisie ni reprendre le focus ensuite.
|
- Vérifier qu'un changement de page avec le focus actif remplace immédiatement le contenu de l'éditeur ; une mise à jour externe de la même page attend le blur sans déplacer la sélection pendant la saisie ni reprendre le focus ensuite.
|
||||||
- Pour les dessins temporaires et l'historique du simulateur de chance, vérifier l'isolation par clé de session lors des changements de page/outil et après rechargement ; une navigation seule ne doit pas écraser les données de l'autre clé.
|
- Pour les dessins temporaires et l'historique du simulateur de chance, vérifier l'isolation par clé de session lors des changements de page/outil et après rechargement ; une navigation seule ne doit pas écraser les données de l'autre clé.
|
||||||
- Pour le simulateur de chance, vérifier que les brouillons suivent le taux et l'outil actifs, restent intacts lors d'un rendu sans rapport et se valident au blur selon le champ.
|
- Pour le simulateur de chance, vérifier que les brouillons suivent le taux et l'outil actifs, restent intacts lors d'un rendu sans rapport et se valident au blur selon le champ.
|
||||||
|
|
@ -81,9 +81,9 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
||||||
- Vérifier les boutons icon, danger, primary et secondary.
|
- Vérifier les boutons icon, danger, primary et secondary.
|
||||||
- Vérifier les cartes, panels et scrollbars.
|
- Vérifier les cartes, panels et scrollbars.
|
||||||
- Vérifier les états hover/focus.
|
- Vérifier les états hover/focus.
|
||||||
- Vérifier mobile et desktop quand le changement touche la mise en page.
|
- Vérifier les fenêtres de 768, 960, 1024, 1280, 1440 et 1920 px quand le changement touche la mise en page.
|
||||||
- Les tests `e2e/card-surfaces.spec.js` compilent le Sass complet et vérifient les
|
- Les tests `e2e/card-surfaces.spec.js` compilent le Sass complet et vérifient les
|
||||||
styles calculés des cartes NDS, home jeux et MHW en desktop/mobile : fonds,
|
styles calculés des cartes NDS, home jeux et MHW en fenêtre large/compacte : fonds,
|
||||||
bordures, séparateurs dorés, hover sans redimensionnement, focus clavier NDS
|
bordures, séparateurs dorés, hover sans redimensionnement, focus clavier NDS
|
||||||
et faces MHW. Ils ne créent pas de snapshots visuels et ne remplacent pas les
|
et faces MHW. Ils ne créent pas de snapshots visuels et ne remplacent pas les
|
||||||
parcours des pages réelles.
|
parcours des pages réelles.
|
||||||
|
|
@ -91,7 +91,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
||||||
complètent cette couverture : tailles et états NDS, panneaux, feedbacks,
|
complètent cette couverture : tailles et états NDS, panneaux, feedbacks,
|
||||||
modales/drawers/toasts, scrollbars, mouvement réduit, états des outils et
|
modales/drawers/toasts, scrollbars, mouvement réduit, états des outils et
|
||||||
masques d'icônes décodables. Les layouts home/À propos/librairie sont aussi
|
masques d'icônes décodables. Les layouts home/À propos/librairie sont aussi
|
||||||
vérifiés sur les pages réelles en desktop, tablette et mobile.
|
vérifiés sur les pages réelles en fenêtre large et demi-écran.
|
||||||
- `tests/static-styles.test.mjs` conserve les frontières d'architecture, les
|
- `tests/static-styles.test.mjs` conserve les frontières d'architecture, les
|
||||||
conventions publiques et les limites de taille des fichiers, pas des preuves
|
conventions publiques et les limites de taille des fichiers, pas des preuves
|
||||||
de rendu basées sur la présence d'un nom de classe ou de mixin dans le Sass.
|
de rendu basées sur la présence d'un nom de classe ou de mixin dans le Sass.
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ test("opening and editing one toolbox only reads its media and reuses it on retu
|
||||||
test("the game drawer loads only the linked toolbox media", async ({ page }) => {
|
test("the game drawer loads only the linked toolbox media", async ({ page }) => {
|
||||||
const seeded = await setup(page);
|
const seeded = await setup(page);
|
||||||
await page.goto("/#/games/mhwilds");
|
await page.goto("/#/games/mhwilds");
|
||||||
await page.getByRole("button", { name: "Afficher la toolbox", exact: true }).click();
|
await page.getByRole("tab", { name: "Toolbox", exact: true }).click();
|
||||||
const drawer = page.locator("#toolbox-drawer .drawer-panel");
|
const drawer = page.locator("#toolbox-drawer .drawer-panel");
|
||||||
await expect(drawer.locator('.image-preview img')).toHaveAttribute("src", seeded.modules["a:images"].images[0].dataUrl);
|
await expect(drawer.locator('.image-preview img')).toHaveAttribute("src", seeded.modules["a:images"].images[0].dataUrl);
|
||||||
await expect(drawer.locator(".annotation-marker")).toHaveCount(1);
|
await expect(drawer.locator(".annotation-marker")).toHaveCount(1);
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,11 @@ async function copyVisibleResults(page) {
|
||||||
|
|
||||||
async function openDrawer(page) {
|
async function openDrawer(page) {
|
||||||
const drawer = page.locator("#toolbox-drawer");
|
const drawer = page.locator("#toolbox-drawer");
|
||||||
if (!await drawer.locator(".drawer-panel").isVisible()) await page.locator(".topbar-actions button").click();
|
if (!await drawer.locator(".drawer-panel").isVisible()) {
|
||||||
|
const compactTrigger = page.getByRole("tab", { name: "Toolbox", exact: true });
|
||||||
|
if (await compactTrigger.isVisible()) await compactTrigger.click();
|
||||||
|
else await page.getByRole("button", { name: "Afficher la toolbox", exact: true }).click();
|
||||||
|
}
|
||||||
await expect(drawer.locator(".drawer-panel")).toBeVisible();
|
await expect(drawer.locator(".drawer-panel")).toBeVisible();
|
||||||
return drawer;
|
return drawer;
|
||||||
}
|
}
|
||||||
|
|
@ -190,7 +194,7 @@ test("filtered results create a checklist in the linked active space without rep
|
||||||
expect(await page.evaluate((key) => globalThis.localStorage.getItem(key), storageKey)).toBe(associationBefore);
|
expect(await page.evaluate((key) => globalThis.localStorage.getItem(key), storageKey)).toBe(associationBefore);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const width of [1280, 390]) {
|
for (const width of [1280, 768]) {
|
||||||
test(`drawer linking, page controls and persistent restoration at ${width}px`, async ({ page }) => {
|
test(`drawer linking, page controls and persistent restoration at ${width}px`, async ({ page }) => {
|
||||||
test.setTimeout(60000);
|
test.setTimeout(60000);
|
||||||
await page.setViewportSize({ width, height: 900 });
|
await page.setViewportSize({ width, height: 900 });
|
||||||
|
|
@ -241,7 +245,7 @@ for (const width of [1280, 390]) {
|
||||||
const headingActionsBounds = await page.locator(".game-heading-actions").boundingBox();
|
const headingActionsBounds = await page.locator(".game-heading-actions").boundingBox();
|
||||||
expect(pageControlsBounds.y).toBeGreaterThanOrEqual(pageHeadingBounds.y);
|
expect(pageControlsBounds.y).toBeGreaterThanOrEqual(pageHeadingBounds.y);
|
||||||
expect(pageControlsBounds.y + pageControlsBounds.height).toBeLessThanOrEqual(pageHeadingBounds.y + pageHeadingBounds.height);
|
expect(pageControlsBounds.y + pageControlsBounds.height).toBeLessThanOrEqual(pageHeadingBounds.y + pageHeadingBounds.height);
|
||||||
if (width > 760) {
|
if (width > 1024) {
|
||||||
expect(headingActionsBounds.height).toBeCloseTo(pageHeadingBounds.height, 0);
|
expect(headingActionsBounds.height).toBeCloseTo(pageHeadingBounds.height, 0);
|
||||||
expect(pageControlsBounds.y).toBeCloseTo(pageHeadingBounds.y, 0);
|
expect(pageControlsBounds.y).toBeCloseTo(pageHeadingBounds.y, 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ test("badges, notices, progress and chips expose meaningful visual states", asyn
|
||||||
await expect(chip.locator("img")).toHaveCSS("height", "16px");
|
await expect(chip.locator("img")).toHaveCSS("height", "16px");
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const width of [1280, 390]) {
|
for (const width of [1440, 1280, 768]) {
|
||||||
test(`overlays fit, scroll internally and preserve stacking at ${width}px`, async ({ page }) => {
|
test(`overlays fit, scroll internally and preserve stacking at ${width}px`, async ({ page }) => {
|
||||||
await page.setViewportSize({ width, height: 800 });
|
await page.setViewportSize({ width, height: 800 });
|
||||||
await mountStyles(page, `<div class="confirm-modal-root">
|
await mountStyles(page, `<div class="confirm-modal-root">
|
||||||
|
|
@ -88,17 +88,17 @@ for (const width of [1280, 390]) {
|
||||||
await page.locator("body").evaluate((element) => element.classList.add("is-modal-open"));
|
await page.locator("body").evaluate((element) => element.classList.add("is-modal-open"));
|
||||||
await expect(page.locator("body")).toHaveCSS("overflow", "hidden");
|
await expect(page.locator("body")).toHaveCSS("overflow", "hidden");
|
||||||
|
|
||||||
await mountStyles(page, `<div class="drawer" aria-hidden="false">
|
await mountStyles(page, `<div class="drawer ${width < 1440 ? "is-compact" : ""}" aria-hidden="false">
|
||||||
<section class="drawer-panel nds-drawer-panel"><div style="height:1400px">Drawer</div></section>
|
<section class="drawer-panel nds-drawer-panel"><div style="height:1400px">Drawer</div></section>
|
||||||
<div class="drawer-resize-handle nds-drawer-resize-handle"></div></div>`);
|
<div class="drawer-resize-handle nds-drawer-resize-handle"></div></div>`);
|
||||||
const drawer = page.locator(".drawer-panel");
|
const drawer = page.locator(".drawer-panel");
|
||||||
await expectFits(drawer);
|
await expectFits(drawer);
|
||||||
await expect(drawer).toHaveCSS("top", width <= 760 ? "110px" : "68px");
|
await expect(drawer).toHaveCSS("top", "68px");
|
||||||
await expect(drawer).toHaveCSS("position", "fixed");
|
await expect(drawer).toHaveCSS("position", "fixed");
|
||||||
await expect(drawer).toHaveCSS("overflow-y", "auto");
|
await expect(drawer).toHaveCSS("overflow-y", "auto");
|
||||||
await expect(drawer).toHaveCSS("backdrop-filter", "blur(10px)");
|
await expect(drawer).toHaveCSS("backdrop-filter", "blur(10px)");
|
||||||
await expect(drawer).toHaveCSS("width", width < 700 ? `${width}px` : "700px");
|
await expect(drawer).toHaveCSS("width", width < 1440 ? `${width - (width <= 1024 ? 72 : 248)}px` : "700px");
|
||||||
await expect(page.locator(".drawer-resize-handle")).toHaveCSS("display", width < 700 ? "none" : "block");
|
await expect(page.locator(".drawer-resize-handle")).toHaveCSS("display", width < 1440 ? "none" : "block");
|
||||||
await page.locator(".drawer").evaluate((element) => element.setAttribute("aria-hidden", "true"));
|
await page.locator(".drawer").evaluate((element) => element.setAttribute("aria-hidden", "true"));
|
||||||
await expect(drawer).toBeHidden();
|
await expect(drawer).toBeHidden();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,29 +35,60 @@ test("calculator layout follows the tool width rather than its location", async
|
||||||
await expect(tool.locator(".calculator-result")).toHaveCSS("min-height", "72px");
|
await expect(tool.locator(".calculator-result")).toHaveCSS("min-height", "72px");
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const width of [1280, 900, 390]) {
|
test("toolbox columns follow the toolbox content width", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 1280, height: 720 });
|
||||||
|
await mountStyles(page, `<section class="toolbox-page" style="width: 920px">
|
||||||
|
<div class="modules-toolbar"><div class="layout-switch">Layout</div></div>
|
||||||
|
<div class="modules" data-layout-cols="2"><div class="module-column"><article class="module">A</article></div><div class="module-column"><article class="module">B</article></div></div>
|
||||||
|
</section>`);
|
||||||
|
const toolbox = page.locator(".toolbox-page");
|
||||||
|
const modules = toolbox.locator(".modules");
|
||||||
|
await columns(modules, 2);
|
||||||
|
await expect(toolbox.locator(".modules-toolbar")).toBeVisible();
|
||||||
|
await toolbox.evaluate((element) => { element.style.width = "899px"; });
|
||||||
|
await columns(modules, 1);
|
||||||
|
await expect(toolbox.locator(".modules-toolbar")).toBeHidden();
|
||||||
|
await expect(toolbox.locator(".module-column").first()).toHaveCSS("display", "contents");
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const width of [1920, 1280, 1025, 1024, 960, 768]) {
|
||||||
test(`home, about and library retain their responsive layout at ${width}px`, async ({ page }) => {
|
test(`home, about and library retain their responsive layout at ${width}px`, async ({ page }) => {
|
||||||
await page.setViewportSize({ width, height: 900 });
|
await page.setViewportSize({ width, height: 900 });
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
const onboarding = page.locator(".home-onboarding");
|
const onboarding = page.locator(".home-onboarding");
|
||||||
await expectFits(onboarding);
|
await expectFits(onboarding);
|
||||||
await columns(page.locator(".home-onboarding-layout"), width <= 760 ? 1 : 2);
|
const hero = page.locator(".hero");
|
||||||
|
const heroContent = hero.locator(":scope > div").first();
|
||||||
|
const heroStats = hero.locator(".hero-stats");
|
||||||
|
await columns(hero, width <= 1024 ? 1 : 2);
|
||||||
|
const [heroContentBounds, heroStatsBounds] = await Promise.all([heroContent.boundingBox(), heroStats.boundingBox()]);
|
||||||
|
if (width <= 1024) {
|
||||||
|
expect(heroStatsBounds.y).toBeGreaterThanOrEqual(heroContentBounds.y + heroContentBounds.height);
|
||||||
|
expect(heroStatsBounds.x).toBeCloseTo(heroContentBounds.x, 0);
|
||||||
|
} else {
|
||||||
|
expect(heroStatsBounds.x).toBeGreaterThan(heroContentBounds.x + heroContentBounds.width);
|
||||||
|
}
|
||||||
|
await columns(page.locator(".home-onboarding-layout"), width <= 1024 ? 1 : 2);
|
||||||
await expect(page.locator(".home-onboarding-slider figcaption")).toHaveCSS("position", "absolute");
|
await expect(page.locator(".home-onboarding-slider figcaption")).toHaveCSS("position", "absolute");
|
||||||
await expect(page.locator(".home-onboarding-dots")).toHaveCSS("display", "flex");
|
await expect(page.locator(".home-onboarding-dots")).toHaveCSS("display", "flex");
|
||||||
await expect(page.locator(".home-onboarding-slider img")).toHaveCSS("object-fit", "cover");
|
await expect(page.locator(".home-onboarding-slider img")).toHaveCSS("object-fit", "cover");
|
||||||
|
await expect(page.locator(".home-visual")).toHaveCSS("display", width <= 1024 ? "none" : "grid");
|
||||||
|
await expect(page.locator(".origin-copy")).toHaveCSS("border-right-width", width <= 1024 ? "0px" : "2px");
|
||||||
|
await columns(page.locator(".origin-section"), width <= 1024 ? 1 : 2);
|
||||||
const side = page.locator(".sidebar");
|
const side = page.locator(".sidebar");
|
||||||
await expect(side).toHaveCSS("display", width <= 760 ? "none" : "flex");
|
await expect(side).toHaveCSS("display", "flex");
|
||||||
if (width > 760) {
|
{
|
||||||
const responsiveCompact = width <= 1024;
|
const responsiveCompact = width <= 1024;
|
||||||
await expect(side).not.toHaveAttribute("data-variant");
|
if (responsiveCompact) await expect(side).toHaveAttribute("data-variant", "compact");
|
||||||
|
else await expect(side).not.toHaveAttribute("data-variant");
|
||||||
await expect(side).toHaveCSS("width", responsiveCompact ? "72px" : "248px");
|
await expect(side).toHaveCSS("width", responsiveCompact ? "72px" : "248px");
|
||||||
await expect(page.locator(".sidebar-mascot")).toHaveCSS("display", responsiveCompact ? "none" : "block");
|
await expect(page.locator(".sidebar-mascot")).toHaveCSS("display", responsiveCompact ? "none" : "block");
|
||||||
const secondaryLinks = page.locator(".sidebar-secondary-links");
|
const secondaryLinks = page.locator(".sidebar-secondary-links");
|
||||||
await expect(secondaryLinks).toHaveCSS("display", "grid");
|
await expect(secondaryLinks).toHaveCSS("display", "grid");
|
||||||
await expect(page.locator(".sidebar-about-link")).toHaveCount(3);
|
await expect(page.locator(".sidebar-about-link")).toHaveCount(3);
|
||||||
await expect(page.locator(".sidebar-about-link strong").first()).toHaveCSS("display", responsiveCompact ? "none" : "block");
|
await expect(page.locator(".sidebar-about-link strong").first()).toHaveCSS("display", responsiveCompact ? "none" : "block");
|
||||||
await expect(page.locator(".sidebar-note")).toHaveCSS("display", responsiveCompact ? "none" : "block");
|
await expect(page.locator(".sidebar-note")).toHaveCSS("display", "block");
|
||||||
await expect(page.locator(".sidebar-storage-actions")).toHaveCSS("flex-direction", "row");
|
await expect(page.locator(".sidebar-storage-actions")).toHaveCSS("flex-direction", responsiveCompact ? "column" : "row");
|
||||||
await expect(page.locator(".sidebar-action-button")).toHaveCount(2);
|
await expect(page.locator(".sidebar-action-button")).toHaveCount(2);
|
||||||
await expect(page.locator(".nav-item")).toHaveCount(3);
|
await expect(page.locator(".nav-item")).toHaveCount(3);
|
||||||
await expect(page.locator(".nav-item strong").first()).toHaveCSS("display", responsiveCompact ? "none" : "block");
|
await expect(page.locator(".nav-item strong").first()).toHaveCSS("display", responsiveCompact ? "none" : "block");
|
||||||
|
|
@ -83,12 +114,26 @@ for (const width of [1280, 900, 390]) {
|
||||||
await expect(page.locator(".about-tool-item .module-icon").first()).toHaveCSS("width", "34px");
|
await expect(page.locator(".about-tool-item .module-icon").first()).toHaveCSS("width", "34px");
|
||||||
await page.goto("/#/library");
|
await page.goto("/#/library");
|
||||||
await expectFits(page.locator(".library-page"));
|
await expectFits(page.locator(".library-page"));
|
||||||
await columns(page.locator(".library-doc-layout"), width <= 760 ? 1 : 2);
|
await columns(page.locator(".library-doc-layout"), width <= 1024 ? 1 : 2);
|
||||||
|
await columns(page.locator(".library-example-layout.has-legend").first(), width <= 1024 ? 1 : 2);
|
||||||
expect(await page.locator("body").evaluate((element) => element.scrollWidth)).toBeLessThanOrEqual(width);
|
expect(await page.locator("body").evaluate((element) => element.scrollWidth)).toBeLessThanOrEqual(width);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const width of [1280, 390]) {
|
for (const width of [640, 390]) {
|
||||||
|
test(`below the validated width the compact sidebar remains available without phone navigation at ${width}px`, async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width, height: 720 });
|
||||||
|
await page.goto("/#/games/aniimo/catalog");
|
||||||
|
await expect(page.locator(".sidebar")).toBeVisible();
|
||||||
|
await expect(page.locator(".sidebar")).toHaveCSS("width", "72px");
|
||||||
|
await expect(page.locator(".mobile-nav")).toHaveCount(0);
|
||||||
|
await expect(page.locator(".route-content-view")).toBeVisible();
|
||||||
|
await expect(page.locator(".compact-game-view-switch")).toBeVisible();
|
||||||
|
expect(await page.evaluate(() => globalThis.document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const width of [1280, 768]) {
|
||||||
test(`game grids and toolbox columns adapt without horizontal overflow at ${width}px`, async ({ page }) => {
|
test(`game grids and toolbox columns adapt without horizontal overflow at ${width}px`, async ({ page }) => {
|
||||||
await page.setViewportSize({ width, height: 900 });
|
await page.setViewportSize({ width, height: 900 });
|
||||||
await mountStyles(page, `<main>
|
await mountStyles(page, `<main>
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ test("cancelled and unreadable replacements preserve the cover and allow selecti
|
||||||
|
|
||||||
test("game association takes precedence without discarding the custom cover", async ({ page }) => {
|
test("game association takes precedence without discarding the custom cover", async ({ page }) => {
|
||||||
await page.goto("/#/games/mhwilds");
|
await page.goto("/#/games/mhwilds");
|
||||||
await page.getByRole("button", { name: "Afficher la toolbox", exact: true }).click();
|
await page.getByRole("tab", { name: "Toolbox", exact: true }).click();
|
||||||
const drawer = page.locator("#toolbox-drawer .drawer-panel");
|
const drawer = page.locator("#toolbox-drawer .drawer-panel");
|
||||||
await importToolbox(page, "Linked cover", drawer);
|
await importToolbox(page, "Linked cover", drawer);
|
||||||
await expect(drawer.getByRole("heading", { name: "Linked cover" })).toBeVisible();
|
await expect(drawer.getByRole("heading", { name: "Linked cover" })).toBeVisible();
|
||||||
|
|
@ -111,7 +111,7 @@ test("game association takes precedence without discarding the custom cover", as
|
||||||
await expect(page.locator(".toolbox-game-icon img")).toHaveAttribute("src", gameCover);
|
await expect(page.locator(".toolbox-game-icon img")).toHaveAttribute("src", gameCover);
|
||||||
await expect(picker(page)).toHaveCount(0);
|
await expect(picker(page)).toHaveCount(0);
|
||||||
await page.goto("/#/games/mhwilds");
|
await page.goto("/#/games/mhwilds");
|
||||||
await page.getByRole("button", { name: "Afficher la toolbox", exact: true }).click();
|
await page.getByRole("tab", { name: "Toolbox", exact: true }).click();
|
||||||
await drawer.getByRole("button", { name: "Lier une toolbox", exact: true }).click();
|
await drawer.getByRole("button", { name: "Lier une toolbox", exact: true }).click();
|
||||||
const dialog = page.getByRole("dialog", { name: "Lier une toolbox", exact: true });
|
const dialog = page.getByRole("dialog", { name: "Lier une toolbox", exact: true });
|
||||||
await dialog.getByLabel("Toolbox associée").selectOption("");
|
await dialog.getByLabel("Toolbox associée").selectOption("");
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
// Rôle : vérifie la réservation hybride, le redimensionnement et le mode mobile du drawer toolbox.
|
// Rôle : vérifie le panneau large et la bascule guide/toolbox en fenêtre compacte.
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
async function openDrawer(page) {
|
async function openDrawer(page) {
|
||||||
const trigger = page.getByRole("button", { name: "Afficher la toolbox", exact: true });
|
await page.locator(".topbar-actions").waitFor();
|
||||||
|
const compactTrigger = page.getByRole("tab", { name: "Toolbox", exact: true });
|
||||||
|
const trigger = await compactTrigger.isVisible()
|
||||||
|
? compactTrigger
|
||||||
|
: page.getByRole("button", { name: "Afficher la toolbox", exact: true });
|
||||||
await expect(trigger).toBeVisible();
|
await expect(trigger).toBeVisible();
|
||||||
await trigger.click();
|
await trigger.click();
|
||||||
await expect(page.getByRole("button", { name: "Masquer la toolbox", exact: true })).toHaveAttribute("aria-pressed", "true");
|
if (await compactTrigger.isVisible()) await expect(compactTrigger).toHaveAttribute("aria-selected", "true");
|
||||||
|
else await expect(page.getByRole("button", { name: "Masquer la toolbox", exact: true })).toHaveAttribute("aria-pressed", "true");
|
||||||
const drawer = page.locator("#toolbox-drawer");
|
const drawer = page.locator("#toolbox-drawer");
|
||||||
await expect(drawer.locator(".drawer-panel")).toBeVisible();
|
await expect(drawer.locator(".drawer-panel")).toBeVisible();
|
||||||
return drawer;
|
return drawer;
|
||||||
|
|
@ -20,8 +25,8 @@ async function resizeDrawer(page, deltaX) {
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
}
|
}
|
||||||
|
|
||||||
test("drawer reserves 700px while additional width overlays the page", async ({ page }) => {
|
test("wide drawer reserves 700px while additional width overlays the page", async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 1280, height: 900 });
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
await page.goto("/#/games/mhwilds/monsters");
|
await page.goto("/#/games/mhwilds/monsters");
|
||||||
|
|
||||||
const drawer = await openDrawer(page);
|
const drawer = await openDrawer(page);
|
||||||
|
|
@ -41,7 +46,7 @@ test("drawer reserves 700px while additional width overlays the page", async ({
|
||||||
const topbarBounds = await page.locator(".topbar").boundingBox();
|
const topbarBounds = await page.locator(".topbar").boundingBox();
|
||||||
expect(initialPanelBounds.width).toBeCloseTo(700, 0);
|
expect(initialPanelBounds.width).toBeCloseTo(700, 0);
|
||||||
expect(initialPanelBounds.x).toBeCloseTo(contentBounds.x + contentBounds.width - 700, 0);
|
expect(initialPanelBounds.x).toBeCloseTo(contentBounds.x + contentBounds.width - 700, 0);
|
||||||
expect(topbarBounds.x + topbarBounds.width).toBeCloseTo(1280, 0);
|
expect(topbarBounds.x + topbarBounds.width).toBeCloseTo(1440, 0);
|
||||||
|
|
||||||
const search = page.locator(".nds-filter-search input").first();
|
const search = page.locator(".nds-filter-search input").first();
|
||||||
await search.fill("rath");
|
await search.fill("rath");
|
||||||
|
|
@ -55,7 +60,7 @@ test("drawer reserves 700px while additional width overlays the page", async ({
|
||||||
expect(expandedPanelBounds.x).toBeCloseTo(initialPanelBounds.x - 100, 0);
|
expect(expandedPanelBounds.x).toBeCloseTo(initialPanelBounds.x - 100, 0);
|
||||||
expect(expandedMainBounds.x).toBeCloseTo(initialMainBounds.x, 0);
|
expect(expandedMainBounds.x).toBeCloseTo(initialMainBounds.x, 0);
|
||||||
expect(expandedMainBounds.width).toBeCloseTo(initialMainBounds.width, 0);
|
expect(expandedMainBounds.width).toBeCloseTo(initialMainBounds.width, 0);
|
||||||
expect(await page.evaluate(() => globalThis.document.documentElement.scrollWidth)).toBeLessThanOrEqual(1280);
|
expect(await page.evaluate(() => globalThis.document.documentElement.scrollWidth)).toBeLessThanOrEqual(1440);
|
||||||
|
|
||||||
await resizeDrawer(page, 300);
|
await resizeDrawer(page, 300);
|
||||||
await expect.poll(async () => (await panel.boundingBox()).width).toBeCloseTo(700, 0);
|
await expect.poll(async () => (await panel.boundingBox()).width).toBeCloseTo(700, 0);
|
||||||
|
|
@ -71,15 +76,55 @@ test("drawer reserves 700px while additional width overlays the page", async ({
|
||||||
await expect.poll(async () => (await reopenedDrawer.locator(".drawer-panel").boundingBox()).width).toBeCloseTo(700, 0);
|
await expect.poll(async () => (await reopenedDrawer.locator(".drawer-panel").boundingBox()).width).toBeCloseTo(700, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("drawer becomes a non-resizable full-width overlay below 700px", async ({ page }) => {
|
test("compact game view switches between mounted guide and toolbox", async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 390, height: 900 });
|
await page.setViewportSize({ width: 960, height: 720 });
|
||||||
await page.goto("/#/games/mhwilds/monsters");
|
await page.goto("/#/games/mhwilds/monsters");
|
||||||
|
|
||||||
|
const guide = page.locator(".route-content-view");
|
||||||
|
const search = page.locator(".nds-filter-search input").first();
|
||||||
|
await search.fill("rath");
|
||||||
|
await page.evaluate(() => globalThis.window.scrollTo(0, 240));
|
||||||
|
const guideScroll = await page.evaluate(() => globalThis.window.scrollY);
|
||||||
|
const viewSwitch = page.locator(".compact-game-view-switch");
|
||||||
|
const guideTab = page.getByRole("tab", { name: "Guide" });
|
||||||
|
const toolboxTab = page.getByRole("tab", { name: "Toolbox", exact: true });
|
||||||
|
await expect(guideTab).toHaveAttribute("aria-selected", "true");
|
||||||
|
await expect(viewSwitch).toHaveCSS("min-width", "184px");
|
||||||
|
await expect(guideTab).toHaveCSS("min-height", "34px");
|
||||||
|
await expect(toolboxTab).toHaveCSS("padding-left", "10px");
|
||||||
|
const [guideBounds, toolboxBounds] = await Promise.all([guideTab.boundingBox(), toolboxTab.boundingBox()]);
|
||||||
|
expect(guideBounds.width).toBeCloseTo(toolboxBounds.width, 0);
|
||||||
|
await expect(guide).toBeVisible();
|
||||||
const drawer = await openDrawer(page);
|
const drawer = await openDrawer(page);
|
||||||
const panel = drawer.locator(".drawer-panel");
|
const panel = drawer.locator(".drawer-panel");
|
||||||
const bounds = await panel.boundingBox();
|
const bounds = await panel.boundingBox();
|
||||||
expect(bounds.x).toBeCloseTo(0, 0);
|
expect(bounds.x).toBeCloseTo(72, 0);
|
||||||
expect(bounds.width).toBeCloseTo(390, 0);
|
expect(bounds.width).toBeCloseTo(888, 0);
|
||||||
await expect(drawer.locator(".drawer-resize-handle")).toHaveCSS("display", "none");
|
await expect(drawer.locator(".drawer-resize-handle")).toHaveCSS("display", "none");
|
||||||
expect(await page.evaluate(() => globalThis.document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
|
await expect(toolboxTab).toHaveAttribute("aria-selected", "true");
|
||||||
|
await expect(guide).toBeHidden();
|
||||||
|
await guideTab.click();
|
||||||
|
await expect(guide).toBeVisible();
|
||||||
|
await expect(drawer).toBeHidden();
|
||||||
|
await expect(drawer).toHaveCount(1);
|
||||||
|
await expect(search).toHaveValue("rath");
|
||||||
|
await expect.poll(() => page.evaluate(() => globalThis.window.scrollY)).toBe(guideScroll);
|
||||||
|
expect(await page.evaluate(() => globalThis.document.documentElement.scrollWidth)).toBeLessThanOrEqual(960);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("compact toolbox intent survives the 1440px transition", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 1439, height: 720 });
|
||||||
|
await page.goto("/#/games/mhwilds/monsters");
|
||||||
|
await openDrawer(page);
|
||||||
|
await page.setViewportSize({ width: 1440, height: 720 });
|
||||||
|
const drawer = page.locator("#toolbox-drawer");
|
||||||
|
await expect(drawer.locator(".drawer-panel")).toBeVisible();
|
||||||
|
await expect(drawer.locator(".drawer-panel")).toHaveCSS("width", "700px");
|
||||||
|
await expect(page.locator(".app-shell")).toHaveClass(/toolbox-drawer-open/);
|
||||||
|
await expect(page.getByRole("button", { name: "Masquer la toolbox", exact: true })).toBeFocused();
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1439, height: 720 });
|
||||||
|
await page.getByRole("tab", { name: "Guide" }).click();
|
||||||
|
await page.setViewportSize({ width: 1440, height: 720 });
|
||||||
|
await expect(drawer).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -193,13 +193,13 @@ test("the linked game drawer shares the persisted active space", async ({ page }
|
||||||
}, { mhwilds: "drawer-spaces" });
|
}, { mhwilds: "drawer-spaces" });
|
||||||
|
|
||||||
await page.goto("/#/games/mhwilds");
|
await page.goto("/#/games/mhwilds");
|
||||||
await page.getByRole("button", { name: "Afficher la toolbox", exact: true }).click();
|
await page.getByRole("tab", { name: "Toolbox", exact: true }).click();
|
||||||
const drawer = page.locator("#toolbox-drawer");
|
const drawer = page.locator("#toolbox-drawer");
|
||||||
await expect(drawer.getByRole("tab", { name: "Boss" })).toHaveAttribute("aria-selected", "true");
|
await expect(drawer.getByRole("tab", { name: "Boss" })).toHaveAttribute("aria-selected", "true");
|
||||||
await expect(drawer.locator("[data-module-id='a']")).toBeVisible();
|
await expect(drawer.locator("[data-module-id='a']")).toBeVisible();
|
||||||
await drawer.getByRole("tab", { name: "Défaut" }).click();
|
await drawer.getByRole("tab", { name: "Défaut" }).click();
|
||||||
await expect(drawer.locator("[data-module-id='a']")).toHaveCount(0);
|
await expect(drawer.locator("[data-module-id='a']")).toHaveCount(0);
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await page.getByRole("button", { name: "Afficher la toolbox", exact: true }).click();
|
await page.getByRole("tab", { name: "Toolbox", exact: true }).click();
|
||||||
await expect(page.locator("#toolbox-drawer").getByRole("tab", { name: "Défaut" })).toHaveAttribute("aria-selected", "true");
|
await expect(page.locator("#toolbox-drawer").getByRole("tab", { name: "Défaut" })).toHaveAttribute("aria-selected", "true");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ test("loads randomizer tabs after module split", async ({ page }) => {
|
||||||
test("creates a game-linked toolbox from the drawer", async ({ page }) => {
|
test("creates a game-linked toolbox from the drawer", async ({ page }) => {
|
||||||
await page.goto("/#/games/mhwilds");
|
await page.goto("/#/games/mhwilds");
|
||||||
await expect(page.getByRole("heading", { name: /Monster Hunter.*Wilds/ })).toBeVisible();
|
await expect(page.getByRole("heading", { name: /Monster Hunter.*Wilds/ })).toBeVisible();
|
||||||
await page.getByRole("button", { name: "Afficher la toolbox", exact: true }).click();
|
await page.getByRole("tab", { name: "Toolbox", exact: true }).click();
|
||||||
|
|
||||||
const drawer = page.locator("#toolbox-drawer .drawer-panel");
|
const drawer = page.locator("#toolbox-drawer .drawer-panel");
|
||||||
await expect(drawer).toBeVisible();
|
await expect(drawer).toBeVisible();
|
||||||
|
|
@ -216,13 +216,13 @@ test("creates a game-linked toolbox from the drawer", async ({ page }) => {
|
||||||
await expect(drawer.getByRole("heading", { name: /Monster Hunter.*Wilds/ })).toBeVisible();
|
await expect(drawer.getByRole("heading", { name: /Monster Hunter.*Wilds/ })).toBeVisible();
|
||||||
await expect(drawer.locator("[data-module-type='memos']")).toBeVisible();
|
await expect(drawer.locator("[data-module-type='memos']")).toBeVisible();
|
||||||
await expect(drawer.locator("[data-module-type='links']")).toBeVisible();
|
await expect(drawer.locator("[data-module-type='links']")).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: "Masquer la toolbox", exact: true })).toBeVisible();
|
await expect(page.getByRole("tab", { name: "Toolbox", exact: true })).toHaveAttribute("aria-selected", "true");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("links an available toolbox directly from the empty game drawer", async ({ page }) => {
|
test("links an available toolbox directly from the empty game drawer", async ({ page }) => {
|
||||||
await createToolbox(page, "Toolbox déjà liée");
|
await createToolbox(page, "Toolbox déjà liée");
|
||||||
await page.goto("/#/games/mhwilds");
|
await page.goto("/#/games/mhwilds");
|
||||||
await page.getByRole("button", { name: "Afficher la toolbox", exact: true }).click();
|
await page.getByRole("tab", { name: "Toolbox", exact: true }).click();
|
||||||
|
|
||||||
const drawer = page.locator("#toolbox-drawer .drawer-panel");
|
const drawer = page.locator("#toolbox-drawer .drawer-panel");
|
||||||
await expect(drawer.getByRole("heading", { name: "Ou associer une toolbox déjà existante" })).toBeVisible();
|
await expect(drawer.getByRole("heading", { name: "Ou associer une toolbox déjà existante" })).toBeVisible();
|
||||||
|
|
@ -232,7 +232,7 @@ test("links an available toolbox directly from the empty game drawer", async ({
|
||||||
|
|
||||||
await createToolbox(page, "Toolbox libre");
|
await createToolbox(page, "Toolbox libre");
|
||||||
await page.goto("/#/games/diablo4");
|
await page.goto("/#/games/diablo4");
|
||||||
await page.getByRole("button", { name: "Afficher la toolbox", exact: true }).click();
|
await page.getByRole("tab", { name: "Toolbox", exact: true }).click();
|
||||||
|
|
||||||
await expect(drawer.getByLabel("Toolbox disponible").getByRole("option", { name: "Toolbox déjà liée" })).toHaveCount(0);
|
await expect(drawer.getByLabel("Toolbox disponible").getByRole("option", { name: "Toolbox déjà liée" })).toHaveCount(0);
|
||||||
await drawer.getByLabel("Toolbox disponible").selectOption({ label: "Toolbox libre" });
|
await drawer.getByLabel("Toolbox disponible").selectOption({ label: "Toolbox libre" });
|
||||||
|
|
|
||||||
|
|
@ -76,14 +76,15 @@ export function validateSiteContent(site) {
|
||||||
"navigation.games",
|
"navigation.games",
|
||||||
"navigation.library",
|
"navigation.library",
|
||||||
"navigation.about",
|
"navigation.about",
|
||||||
"navigation.mobileLibrary",
|
|
||||||
"navigation.mobileGames",
|
|
||||||
"sidebar.badge",
|
"sidebar.badge",
|
||||||
"sidebar.note",
|
"sidebar.note",
|
||||||
"topbar.dashboard",
|
"topbar.dashboard",
|
||||||
"topbar.toolbox",
|
"topbar.toolbox",
|
||||||
"topbar.library",
|
"topbar.library",
|
||||||
"topbar.games",
|
"topbar.games",
|
||||||
|
"topbar.gameViewLabel",
|
||||||
|
"topbar.guideView",
|
||||||
|
"topbar.toolboxView",
|
||||||
"gamesPage.eyebrow",
|
"gamesPage.eyebrow",
|
||||||
"gamesPage.title",
|
"gamesPage.title",
|
||||||
"gamesPage.description",
|
"gamesPage.description",
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,9 @@ test("vite entrypoint and app shell are wired", async () => {
|
||||||
assert.match(shell, /dragon-sidebar\.png/);
|
assert.match(shell, /dragon-sidebar\.png/);
|
||||||
assert.match(shell, /ImportButton/);
|
assert.match(shell, /ImportButton/);
|
||||||
assert.match(shell, /compactSidebar/);
|
assert.match(shell, /compactSidebar/);
|
||||||
assert.match(shell, /compactSidebar = Boolean\(drawerGameId\)/);
|
assert.match(shell, /reducedSidebar/);
|
||||||
|
assert.match(shell, /compact-game-view-switch/);
|
||||||
|
assert.doesNotMatch(shell, /mobile-nav/);
|
||||||
assert.doesNotMatch(shell, /import\.meta\.env\.DEV \|\| Boolean\(drawerGameId\)/);
|
assert.doesNotMatch(shell, /import\.meta\.env\.DEV \|\| Boolean\(drawerGameId\)/);
|
||||||
assert.match(shell, /data-variant=\{compactSidebar \? "compact"/);
|
assert.match(shell, /data-variant=\{compactSidebar \? "compact"/);
|
||||||
assert.match(shell, /toolbox-drawer-open/);
|
assert.match(shell, /toolbox-drawer-open/);
|
||||||
|
|
|
||||||
14
tests/toolbox-alert-visibility.test.mjs
Normal file
14
tests/toolbox-alert-visibility.test.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Rôle : vérifie que les alertes visibles suivent la vue toolbox réellement affichée.
|
||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { getVisibleToolboxIds } from "../website/src/hooks/useToolboxAlerts.js";
|
||||||
|
|
||||||
|
test("a mounted compact drawer is visible only while its toolbox view is selected", () => {
|
||||||
|
const links = { mhwilds: "linked" };
|
||||||
|
assert.deepEqual([...getVisibleToolboxIds("/games/mhwilds/monsters", "", links)], []);
|
||||||
|
assert.deepEqual([...getVisibleToolboxIds("/games/mhwilds/monsters", "mhwilds", links)], ["linked"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a full toolbox page remains visible independently of the game drawer", () => {
|
||||||
|
assert.deepEqual([...getVisibleToolboxIds("/toolbox/personal", "", {})], ["personal"]);
|
||||||
|
});
|
||||||
|
|
@ -8,9 +8,7 @@
|
||||||
"toolboxes": "Toolboxes",
|
"toolboxes": "Toolboxes",
|
||||||
"games": "Jeux",
|
"games": "Jeux",
|
||||||
"library": "Librairie",
|
"library": "Librairie",
|
||||||
"about": "C'est quoi Sokko G ?",
|
"about": "C'est quoi Sokko G ?"
|
||||||
"mobileLibrary": "Lib.",
|
|
||||||
"mobileGames": "Infos"
|
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"badge": "Local only",
|
"badge": "Local only",
|
||||||
|
|
@ -20,7 +18,10 @@
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"toolbox": "Toolboxes",
|
"toolbox": "Toolboxes",
|
||||||
"library": "Librairie",
|
"library": "Librairie",
|
||||||
"games": "Informations jeu"
|
"games": "Informations jeu",
|
||||||
|
"gameViewLabel": "Vue de la page jeu",
|
||||||
|
"guideView": "Guide",
|
||||||
|
"toolboxView": "Toolbox"
|
||||||
},
|
},
|
||||||
"gamesPage": {
|
"gamesPage": {
|
||||||
"eyebrow": "Pages informatives",
|
"eyebrow": "Pages informatives",
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,48 @@
|
||||||
// Rôle : structure le layout global avec sidebar, topbar, navigation et contenu.
|
// Rôle : structure le layout global avec sidebar, topbar, navigation et contenu.
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
import { Icon } from "./Icon.jsx";
|
import { Icon } from "./Icon.jsx";
|
||||||
import { ImportButton } from "./ImportButton.jsx";
|
import { ImportButton } from "./ImportButton.jsx";
|
||||||
import { Tooltip } from "./Tooltip.jsx";
|
import { Tooltip } from "./Tooltip.jsx";
|
||||||
|
|
||||||
export function Shell({ route, content, games, drawerGameId = "", actions, children }) {
|
export function Shell({ route, content, games, drawerGameId = "", drawerVisible = false, compactGameLayout = false, reducedSidebar = false, actions, children }) {
|
||||||
const gameId = route.split("/")[1] === "games" ? route.split("/")[2] || "" : "";
|
const gameId = route.split("/")[1] === "games" ? route.split("/")[2] || "" : "";
|
||||||
const game = games.find((item) => item.id === gameId);
|
const game = games.find((item) => item.id === gameId);
|
||||||
const topbarLabel = route === "/design-system" && import.meta.env.DEV ? "Design system" : route.startsWith("/toolbox") ? content.topbar.toolbox : route.startsWith("/games") ? content.topbar.games : route === "/library" ? content.topbar.library : content.topbar.dashboard;
|
const topbarLabel = route === "/design-system" && import.meta.env.DEV ? "Design system" : route.startsWith("/toolbox") ? content.topbar.toolbox : route.startsWith("/games") ? content.topbar.games : route === "/library" ? content.topbar.library : content.topbar.dashboard;
|
||||||
const toolboxOpen = Boolean(game && drawerGameId === game.id);
|
const toolboxOpen = Boolean(game && drawerGameId === game.id && drawerVisible);
|
||||||
const toolboxLabel = toolboxOpen ? "Masquer la toolbox" : "Afficher la toolbox";
|
const toolboxLabel = toolboxOpen ? "Masquer la toolbox" : "Afficher la toolbox";
|
||||||
const compactSidebar = Boolean(drawerGameId);
|
const compactSidebar = Boolean(reducedSidebar || (!compactGameLayout && toolboxOpen));
|
||||||
|
const compactToolboxVisible = Boolean(compactGameLayout && toolboxOpen);
|
||||||
|
const guideTabRef = useRef(null);
|
||||||
|
const toolboxTabRef = useRef(null);
|
||||||
|
const wideToolboxButtonRef = useRef(null);
|
||||||
|
const focusedViewControlRef = useRef("");
|
||||||
|
const previousCompactLayoutRef = useRef(compactGameLayout);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const trackFocus = (event) => {
|
||||||
|
if (event.target.closest?.(".compact-game-view-switch")) focusedViewControlRef.current = "compact";
|
||||||
|
else if (event.target.closest?.(".toolbox-button")) focusedViewControlRef.current = "wide";
|
||||||
|
else focusedViewControlRef.current = "";
|
||||||
|
};
|
||||||
|
document.addEventListener("focusin", trackFocus);
|
||||||
|
return () => document.removeEventListener("focusin", trackFocus);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
const layoutChanged = previousCompactLayoutRef.current !== compactGameLayout;
|
||||||
|
previousCompactLayoutRef.current = compactGameLayout;
|
||||||
|
if (layoutChanged && compactGameLayout && focusedViewControlRef.current === "wide") {
|
||||||
|
(compactToolboxVisible ? toolboxTabRef.current : guideTabRef.current)?.focus();
|
||||||
|
} else if (compactGameLayout && !compactToolboxVisible && activeElement?.closest?.("#toolbox-drawer")) {
|
||||||
|
guideTabRef.current?.focus();
|
||||||
|
} else if (layoutChanged && !compactGameLayout && focusedViewControlRef.current === "compact") {
|
||||||
|
wideToolboxButtonRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [compactGameLayout, compactToolboxVisible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`app-shell ${compactSidebar ? "sidebar-compact" : ""} ${drawerGameId ? "toolbox-drawer-open" : ""}`}>
|
<div className={`app-shell ${compactSidebar ? "sidebar-compact" : ""} ${!compactGameLayout && toolboxOpen ? "toolbox-drawer-open" : ""} ${compactToolboxVisible ? "compact-toolbox-visible" : ""}`}>
|
||||||
<aside className="sidebar" data-variant={compactSidebar ? "compact" : undefined}>
|
<aside className="sidebar" data-variant={compactSidebar ? "compact" : undefined}>
|
||||||
<a className="brand" href="#/" aria-label={content.brand.homeAriaLabel}>
|
<a className="brand" href="#/" aria-label={content.brand.homeAriaLabel}>
|
||||||
<span className="brand-mark" aria-hidden="true" />
|
<span className="brand-mark" aria-hidden="true" />
|
||||||
|
|
@ -47,8 +77,12 @@ export function Shell({ route, content, games, drawerGameId = "", actions, child
|
||||||
<div><strong>{topbarLabel}</strong></div>
|
<div><strong>{topbarLabel}</strong></div>
|
||||||
{game && (
|
{game && (
|
||||||
<div className="topbar-actions">
|
<div className="topbar-actions">
|
||||||
<button
|
{compactGameLayout ? <div className="compact-game-view-switch nds-radio-button-group" role="tablist" aria-label={content.topbar.gameViewLabel}>
|
||||||
|
<button ref={guideTabRef} type="button" role="tab" aria-selected={!compactToolboxVisible} className={!compactToolboxVisible ? "active" : ""} onClick={() => actions.setDrawerGameId("")}>{content.topbar.guideView}</button>
|
||||||
|
<button ref={toolboxTabRef} type="button" role="tab" aria-selected={compactToolboxVisible} className={compactToolboxVisible ? "active" : ""} onClick={() => actions.setDrawerGameId(game.id)}>{content.topbar.toolboxView}</button>
|
||||||
|
</div> : <button
|
||||||
className={`drawer-button toolbox-button nds-button ${toolboxOpen ? "active" : ""}`}
|
className={`drawer-button toolbox-button nds-button ${toolboxOpen ? "active" : ""}`}
|
||||||
|
ref={wideToolboxButtonRef}
|
||||||
data-size="sm"
|
data-size="sm"
|
||||||
data-variant="primary"
|
data-variant="primary"
|
||||||
onClick={() => actions.setDrawerGameId(toolboxOpen ? "" : game.id)}
|
onClick={() => actions.setDrawerGameId(toolboxOpen ? "" : game.id)}
|
||||||
|
|
@ -58,19 +92,12 @@ export function Shell({ route, content, games, drawerGameId = "", actions, child
|
||||||
>
|
>
|
||||||
<img src="/static/icons/toolbox.svg" alt="" aria-hidden="true" />
|
<img src="/static/icons/toolbox.svg" alt="" aria-hidden="true" />
|
||||||
<span>{toolboxLabel}</span>
|
<span>{toolboxLabel}</span>
|
||||||
</button>
|
</button>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
</div>
|
</div>
|
||||||
<nav className="mobile-nav" aria-label="Navigation mobile">
|
|
||||||
<a className={route === "/" ? "active" : ""} href="#/">{content.navigation.home}</a>
|
|
||||||
<a className={route.startsWith("/toolboxes") || route.startsWith("/toolbox") ? "active" : ""} href="#/toolboxes">{content.navigation.toolboxes}</a>
|
|
||||||
<button className="nds-button" data-size="sm" data-variant="primary" onClick={() => actions.setCreateModal({})}>+</button>
|
|
||||||
<a className={route === "/library" ? "active" : ""} href="#/library">{content.navigation.mobileLibrary}</a>
|
|
||||||
<a className={route.startsWith("/games") ? "active" : ""} href="#/games">{content.navigation.mobileGames}</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, actions, storageUsage, storageReady = true, getModuleData, updateToolbox, updateModuleData, addImageFiles }) {
|
export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, actions, storageUsage, storageReady = true, getModuleData, updateToolbox, updateModuleData, addImageFiles, visible = true, compact = false }) {
|
||||||
const checklist = useGuideChecklist();
|
const checklist = useGuideChecklist();
|
||||||
const guideChecklist = checklist.available && checklist.guideId.split("/")[0] === gameId ? checklist : undefined;
|
const guideChecklist = checklist.available && checklist.guideId.split("/")[0] === gameId ? checklist : undefined;
|
||||||
const selectedId = links[gameId] || "";
|
const selectedId = links[gameId] || "";
|
||||||
|
|
@ -483,7 +483,7 @@ export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, act
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="drawer" id="toolbox-drawer" aria-hidden="false" style={{ "--drawer-panel-width": `${width}px` }}>
|
<aside className={`drawer ${compact ? "is-compact" : ""}`} id="toolbox-drawer" aria-hidden={!visible} inert={!visible || undefined} style={{ "--drawer-panel-width": `${width}px` }}>
|
||||||
<span className="drawer-resize-handle nds-drawer-resize-handle" aria-hidden="true" onPointerDown={startResize} />
|
<span className="drawer-resize-handle nds-drawer-resize-handle" aria-hidden="true" onPointerDown={startResize} />
|
||||||
<section className="drawer-panel nds-drawer-panel legacy-scrollbar" ref={panelRef}>
|
<section className="drawer-panel nds-drawer-panel legacy-scrollbar" ref={panelRef}>
|
||||||
<div className="drawer-content">
|
<div className="drawer-content">
|
||||||
|
|
|
||||||
16
website/src/hooks/useMediaQuery.js
Normal file
16
website/src/hooks/useMediaQuery.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
// Rôle : expose l'état réactif d'une media query pour synchroniser comportement et mise en page.
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function useMediaQuery(query) {
|
||||||
|
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQuery = window.matchMedia(query);
|
||||||
|
const update = () => setMatches(mediaQuery.matches);
|
||||||
|
update();
|
||||||
|
mediaQuery.addEventListener("change", update);
|
||||||
|
return () => mediaQuery.removeEventListener("change", update);
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import { getCalendarAlertOffsets, getCurrentCalendarOccurrence, getNextCalendarA
|
||||||
import { getCountdownTargetMs } from "../features/toolboxes/modules/timerUtils.js";
|
import { getCountdownTargetMs } from "../features/toolboxes/modules/timerUtils.js";
|
||||||
import { moduleStorageKey } from "../features/toolboxes/storage/toolboxStorage.js";
|
import { moduleStorageKey } from "../features/toolboxes/storage/toolboxStorage.js";
|
||||||
|
|
||||||
function getVisibleToolboxIds(route, drawerGameId, links) {
|
export function getVisibleToolboxIds(route, drawerGameId, links) {
|
||||||
const visibleIds = new Set();
|
const visibleIds = new Set();
|
||||||
const toolboxMatch = route.match(/^\/toolbox\/([^/]+)/);
|
const toolboxMatch = route.match(/^\/toolbox\/([^/]+)/);
|
||||||
if (toolboxMatch?.[1]) visibleIds.add(toolboxMatch[1]);
|
if (toolboxMatch?.[1]) visibleIds.add(toolboxMatch[1]);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { useHashRoute } from "./router/hashRouter.js";
|
||||||
import { normalizeCalendarData, normalizeTimerData } from "./features/toolboxes/storage/toolboxStorage.js";
|
import { normalizeCalendarData, normalizeTimerData } from "./features/toolboxes/storage/toolboxStorage.js";
|
||||||
import { useAutoBackup } from "./features/toolboxes/storage/useAutoBackup.js";
|
import { useAutoBackup } from "./features/toolboxes/storage/useAutoBackup.js";
|
||||||
import { GlobalMemosTicker } from "./features/toolboxes/GlobalMemosTicker.jsx";
|
import { GlobalMemosTicker } from "./features/toolboxes/GlobalMemosTicker.jsx";
|
||||||
|
import { useMediaQuery } from "./hooks/useMediaQuery.js";
|
||||||
|
|
||||||
const assetRecoveryUrl = new URL(window.location.href);
|
const assetRecoveryUrl = new URL(window.location.href);
|
||||||
if (assetRecoveryUrl.searchParams.has("_asset-recovery")) {
|
if (assetRecoveryUrl.searchParams.has("_asset-recovery")) {
|
||||||
|
|
@ -43,6 +44,8 @@ function getNotificationDuration(message, options = {}) {
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const route = useHashRoute();
|
const route = useHashRoute();
|
||||||
|
const compactGameLayout = useMediaQuery("(max-width: 1439px)");
|
||||||
|
const reducedSidebar = useMediaQuery("(max-width: 1024px)");
|
||||||
const { siteContent, siteContentError, games, gamesError, mhwilds, diablo4, aniimo } = useAppData(route);
|
const { siteContent, siteContentError, games, gamesError, mhwilds, diablo4, aniimo } = useAppData(route);
|
||||||
const [filters, setFilters] = useGameFilters();
|
const [filters, setFilters] = useGameFilters();
|
||||||
const [confirmModal, setConfirmModal] = useState(null);
|
const [confirmModal, setConfirmModal] = useState(null);
|
||||||
|
|
@ -50,7 +53,8 @@ function App() {
|
||||||
const [autoBackupModalOpen, setAutoBackupModalOpen] = useState(false);
|
const [autoBackupModalOpen, setAutoBackupModalOpen] = useState(false);
|
||||||
const [linkModalGameId, setLinkModalGameId] = useState("");
|
const [linkModalGameId, setLinkModalGameId] = useState("");
|
||||||
const [drawerGameId, setDrawerGameId] = useState("");
|
const [drawerGameId, setDrawerGameId] = useState("");
|
||||||
const [drawerRoute, setDrawerRoute] = useState(route);
|
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||||
|
const [drawerContext, setDrawerContext] = useState({ route, compactGameLayout });
|
||||||
const [image, setImage] = useState(null);
|
const [image, setImage] = useState(null);
|
||||||
const [pendingTutorialToolboxId, setPendingTutorialToolboxId] = useState("");
|
const [pendingTutorialToolboxId, setPendingTutorialToolboxId] = useState("");
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
|
|
@ -61,11 +65,33 @@ function App() {
|
||||||
const autoBackup = useAutoBackup(store);
|
const autoBackup = useAutoBackup(store);
|
||||||
const { backupNeeded, requestPermission: requestBackupPermission } = autoBackup;
|
const { backupNeeded, requestPermission: requestBackupPermission } = autoBackup;
|
||||||
const guideChecklistStorage = useGuideChecklistStorage(store);
|
const guideChecklistStorage = useGuideChecklistStorage(store);
|
||||||
|
const guideScrollRef = useRef(0);
|
||||||
|
|
||||||
if (drawerRoute !== route) {
|
const setGameToolbox = useCallback((gameId) => {
|
||||||
setDrawerRoute(route);
|
if (gameId) {
|
||||||
|
if (compactGameLayout && !drawerVisible) guideScrollRef.current = window.scrollY;
|
||||||
|
setDrawerGameId(gameId);
|
||||||
|
setDrawerVisible(true);
|
||||||
|
if (compactGameLayout) window.requestAnimationFrame(() => window.scrollTo({ top: 0, behavior: "auto" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDrawerVisible(false);
|
||||||
|
if (compactGameLayout) {
|
||||||
|
window.requestAnimationFrame(() => window.scrollTo({ top: guideScrollRef.current, behavior: "auto" }));
|
||||||
|
} else {
|
||||||
|
setDrawerGameId("");
|
||||||
|
}
|
||||||
|
}, [compactGameLayout, drawerVisible]);
|
||||||
|
|
||||||
|
if (drawerContext.route !== route || drawerContext.compactGameLayout !== compactGameLayout) {
|
||||||
|
setDrawerContext({ route, compactGameLayout });
|
||||||
const activeGameId = route.match(/^\/games\/([^/]+)/)?.[1] || "";
|
const activeGameId = route.match(/^\/games\/([^/]+)/)?.[1] || "";
|
||||||
if (activeGameId !== drawerGameId) setDrawerGameId("");
|
if (activeGameId !== drawerGameId) {
|
||||||
|
setDrawerGameId("");
|
||||||
|
setDrawerVisible(false);
|
||||||
|
} else if (!compactGameLayout && !drawerVisible && drawerGameId) {
|
||||||
|
setDrawerGameId("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const notify = useCallback((message, options = {}) => {
|
const notify = useCallback((message, options = {}) => {
|
||||||
|
|
@ -126,7 +152,7 @@ function App() {
|
||||||
setCreateModal,
|
setCreateModal,
|
||||||
setConfirmModal,
|
setConfirmModal,
|
||||||
setLinkModalGameId,
|
setLinkModalGameId,
|
||||||
setDrawerGameId,
|
setDrawerGameId: setGameToolbox,
|
||||||
setImage
|
setImage
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -135,7 +161,7 @@ function App() {
|
||||||
moduleData: store.moduleData,
|
moduleData: store.moduleData,
|
||||||
links: store.links,
|
links: store.links,
|
||||||
route,
|
route,
|
||||||
drawerGameId,
|
drawerGameId: drawerVisible ? drawerGameId : "",
|
||||||
normalizeTimerData,
|
normalizeTimerData,
|
||||||
normalizeCalendarData,
|
normalizeCalendarData,
|
||||||
textContent: siteContent?.toolboxes?.modules,
|
textContent: siteContent?.toolboxes?.modules,
|
||||||
|
|
@ -173,9 +199,19 @@ function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GuideChecklistContext.Provider value={{ route, aniimo, mhwilds, diablo4, siteContent, toolboxes: store.toolboxes, links: store.links, moduleData: store.moduleData, guideChecklistStorage, actions }}>
|
<GuideChecklistContext.Provider value={{ route, aniimo, mhwilds, diablo4, siteContent, toolboxes: store.toolboxes, links: store.links, moduleData: store.moduleData, guideChecklistStorage, actions }}>
|
||||||
<Shell route={route} content={siteContent} games={games} toolboxes={store.toolboxes} links={store.links} drawerGameId={drawerGameId} actions={actions}>
|
<Shell
|
||||||
<AppErrorBoundary route={route}>
|
route={route}
|
||||||
<RouteContent
|
content={siteContent}
|
||||||
|
games={games}
|
||||||
|
drawerGameId={drawerGameId}
|
||||||
|
drawerVisible={drawerVisible}
|
||||||
|
compactGameLayout={compactGameLayout}
|
||||||
|
reducedSidebar={reducedSidebar}
|
||||||
|
actions={actions}
|
||||||
|
>
|
||||||
|
<div className="route-content-view" aria-hidden={compactGameLayout && drawerVisible || undefined} inert={compactGameLayout && drawerVisible || undefined}>
|
||||||
|
<AppErrorBoundary route={route}>
|
||||||
|
<RouteContent
|
||||||
route={route}
|
route={route}
|
||||||
siteContent={siteContent}
|
siteContent={siteContent}
|
||||||
games={games}
|
games={games}
|
||||||
|
|
@ -202,8 +238,9 @@ function App() {
|
||||||
addImageFiles={actions.addImageFiles}
|
addImageFiles={actions.addImageFiles}
|
||||||
pendingTutorialToolboxId={pendingTutorialToolboxId}
|
pendingTutorialToolboxId={pendingTutorialToolboxId}
|
||||||
setPendingTutorialToolboxId={setPendingTutorialToolboxId}
|
setPendingTutorialToolboxId={setPendingTutorialToolboxId}
|
||||||
/>
|
/>
|
||||||
</AppErrorBoundary>
|
</AppErrorBoundary>
|
||||||
|
</div>
|
||||||
{drawerGameId && (
|
{drawerGameId && (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<ToolboxDrawer
|
<ToolboxDrawer
|
||||||
|
|
@ -219,6 +256,8 @@ function App() {
|
||||||
updateToolbox={actions.updateToolbox}
|
updateToolbox={actions.updateToolbox}
|
||||||
updateModuleData={store.updateModuleData}
|
updateModuleData={store.updateModuleData}
|
||||||
addImageFiles={actions.addImageFiles}
|
addImageFiles={actions.addImageFiles}
|
||||||
|
visible={drawerVisible}
|
||||||
|
compact={compactGameLayout}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
|
@ -238,7 +277,7 @@ function App() {
|
||||||
setCreateModal={setCreateModal}
|
setCreateModal={setCreateModal}
|
||||||
linkModalGameId={linkModalGameId}
|
linkModalGameId={linkModalGameId}
|
||||||
setLinkModalGameId={setLinkModalGameId}
|
setLinkModalGameId={setLinkModalGameId}
|
||||||
setDrawerGameId={setDrawerGameId}
|
setDrawerGameId={setGameToolbox}
|
||||||
storageError={storageError}
|
storageError={storageError}
|
||||||
setStorageError={setStorageError}
|
setStorageError={setStorageError}
|
||||||
image={image}
|
image={image}
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,3 @@
|
||||||
height: 14px;
|
height: 14px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-nav {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,69 @@
|
||||||
.about-reminders-list {
|
.about-reminders-list {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-onboarding-layout,
|
||||||
|
.library-doc-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-example-layout,
|
||||||
|
.hero,
|
||||||
|
.origin-section {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
align-self: start;
|
||||||
|
justify-self: start;
|
||||||
|
width: min(520px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-visual {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.origin-copy {
|
||||||
|
padding-right: 0;
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-doc-nav {
|
||||||
|
position: static;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-heading,
|
||||||
|
.toolbox-head {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-heading-actions,
|
||||||
|
.guide-checklist-page-controls {
|
||||||
|
width: 100%;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-checklist-page-actions,
|
||||||
|
.toolbar,
|
||||||
|
.page-heading .actions,
|
||||||
|
.toolbox-head .actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1439px) {
|
||||||
|
main {
|
||||||
|
width: min(100% - 40px, 1280px);
|
||||||
|
padding-top: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar,
|
||||||
|
.page-heading .actions,
|
||||||
|
.toolbox-head .actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-height: 760px) and (min-width: 1025px) {
|
@media (max-height: 760px) and (min-width: 1025px) {
|
||||||
|
|
@ -58,22 +121,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.drawer {
|
.topbar {
|
||||||
--drawer-top-offset: 110px;
|
padding-inline: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-shell {
|
.topbar:has(.compact-game-view-switch) > div:first-child {
|
||||||
display: block;
|
|
||||||
padding-bottom: 76px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar,
|
|
||||||
.page-heading,
|
|
||||||
.toolbox-head,
|
|
||||||
.module .module-header {
|
.module .module-header {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -109,24 +164,6 @@
|
||||||
order: 2;
|
order: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar {
|
|
||||||
padding: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar-actions {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar-actions > * {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-heading-actions,
|
|
||||||
.guide-checklist-page-controls {
|
|
||||||
width: 100%;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guide-checklist-page-controls {
|
.guide-checklist-page-controls {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
@ -144,11 +181,8 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero,
|
|
||||||
.game-hero,
|
.game-hero,
|
||||||
.page-hero,
|
.page-hero,
|
||||||
.origin-section,
|
|
||||||
.home-onboarding-layout,
|
|
||||||
.home-onboarding-cards,
|
.home-onboarding-cards,
|
||||||
.about-story-section,
|
.about-story-section,
|
||||||
.about-story-list,
|
.about-story-list,
|
||||||
|
|
@ -231,10 +265,6 @@
|
||||||
-webkit-mask-image: linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, 0.14) 30%, #000 70%);
|
-webkit-mask-image: linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, 0.14) 30%, #000 70%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-visual {
|
|
||||||
width: min(320px, 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.origin-section {
|
.origin-section {
|
||||||
padding: var(--space-5);
|
padding: var(--space-5);
|
||||||
}
|
}
|
||||||
|
|
@ -243,13 +273,6 @@
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.origin-copy {
|
|
||||||
padding-right: 0;
|
|
||||||
padding-bottom: var(--space-5);
|
|
||||||
border-right: 0;
|
|
||||||
border-bottom: 2px solid rgba(246, 196, 83, 0.66);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogue-line {
|
.dialogue-line {
|
||||||
max-width: 92%;
|
max-width: 92%;
|
||||||
}
|
}
|
||||||
|
|
@ -322,7 +345,6 @@
|
||||||
max-height: none;
|
max-height: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.library-example-layout,
|
|
||||||
.library-tool-feature-list dl > div {
|
.library-tool-feature-list dl > div {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
@ -413,59 +435,12 @@
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-nav {
|
|
||||||
position: fixed;
|
|
||||||
right: 12px;
|
|
||||||
bottom: 12px;
|
|
||||||
left: 12px;
|
|
||||||
z-index: 60;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 52px 1fr;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
background: rgba(11, 14, 27, 0.9);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-nav a,
|
|
||||||
.mobile-nav button {
|
|
||||||
min-height: 44px;
|
|
||||||
padding: 0 8px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-nav a {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-nav a.active {
|
|
||||||
background: var(--color-primary-soft);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 699px) {
|
@media (max-width: 767px) {
|
||||||
.drawer-panel {
|
|
||||||
width: 100vw;
|
|
||||||
min-width: 0;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-link-existing form {
|
.drawer-link-existing form {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-resize-handle {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
|
|
|
||||||
|
|
@ -359,7 +359,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 700px) {
|
@media (min-width: 1440px) {
|
||||||
.app-shell.toolbox-drawer-open .content-shell {
|
.app-shell.toolbox-drawer-open .content-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 700px;
|
grid-template-columns: minmax(0, 1fr) 700px;
|
||||||
|
|
@ -404,9 +404,19 @@
|
||||||
|
|
||||||
.topbar-actions {
|
.topbar-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compact-game-view-switch {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 184px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-game-view-switch > button {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbox-button {
|
.toolbox-button {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
@ -426,3 +436,11 @@ main {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: var(--space-8) 0 var(--space-12);
|
padding: var(--space-8) 0 var(--space-12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.route-content-view {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell.compact-toolbox-visible .route-content-view {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,4 +57,3 @@
|
||||||
max-width: 920px;
|
max-width: 920px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,3 +99,22 @@ body.is-resizing-drawer * {
|
||||||
.drawer-panel .modules {
|
.drawer-panel .modules {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1439px) {
|
||||||
|
.drawer.is-compact .drawer-panel {
|
||||||
|
inset: 68px 0 0 248px;
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer.is-compact .drawer-resize-handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.drawer.is-compact .drawer-panel {
|
||||||
|
left: 72px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,11 +90,11 @@
|
||||||
.global-memos-ticker { left: 84px; }
|
.global-memos-ticker { left: 84px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 767px) {
|
||||||
.global-memos-ticker {
|
.global-memos-ticker {
|
||||||
right: 12px;
|
right: 12px;
|
||||||
bottom: 84px;
|
bottom: 12px;
|
||||||
left: 12px;
|
left: 84px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.memos-settings label {
|
.memos-settings label {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbox-page {
|
||||||
|
container-name: toolbox-page;
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
.modules[data-layout-cols="1"] {
|
.modules[data-layout-cols="1"] {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +26,20 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@container toolbox-page (max-width: 899px) {
|
||||||
|
.modules-toolbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modules[data-layout-cols="2"] {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modules[data-layout-cols="2"] .module-column {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.module-column.is-empty-warning {
|
.module-column.is-empty-warning {
|
||||||
min-height: 132px;
|
min-height: 132px;
|
||||||
border: 1px dashed rgba(246, 196, 83, 0.26);
|
border: 1px dashed rgba(246, 196, 83, 0.26);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue