Improve compact desktop usability and action guidance
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
0ac1a0974e
commit
0fc18cd8b9
24 changed files with 325 additions and 90 deletions
|
|
@ -17,6 +17,7 @@ Lire le fichier d'instructions le plus proche du code modifié avant d'interveni
|
|||
## Règles globales
|
||||
|
||||
- Utiliser `rg`, `git status --short`, `git diff --stat` et les commandes ciblées avant les recherches larges.
|
||||
- Concevoir l'interface pour un ordinateur utilisé en plein écran ou en demi-écran, avec `768px` comme largeur minimale validée. Ne pas créer de navigation, de composant ou de parcours propre au téléphone. Sous `768px`, préserver seulement l'accès au contenu et empêcher les débordements globaux, sans optimiser le confort d'usage.
|
||||
- Ne pas réintroduire de fallback massif type `DEFAULT_SITE_CONTENT` dans le code React.
|
||||
- Chaque nouveau fichier source ou test commence par un commentaire court `Rôle : ...` dans le style du langage.
|
||||
- Ne pas lancer automatiquement un serveur sur `5174` si `5173` répond déjà.
|
||||
|
|
|
|||
|
|
@ -161,6 +161,8 @@ Les heroes de la page d'accueil et des pages jeux utilisent le meme style via `.
|
|||
|
||||
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.
|
||||
|
||||
L'action principale dépend des données locales : sans toolbox, « Créer ma première toolbox » ouvre directement la création guidée ; dès qu'une toolbox existe, « Voir mes toolboxes » mène à la liste. « Voir les jeux » reste l'action secondaire.
|
||||
|
||||
Caracteristiques :
|
||||
|
||||
- fond commun nebula sombre ;
|
||||
|
|
@ -573,7 +575,7 @@ Sidebar :
|
|||
- icones SVG pour Accueil, Toolboxes et Jeux ;
|
||||
- navigation active en violet soft ;
|
||||
- le logo controller garde le gradient de l'ancien badge et une rotation au hover.
|
||||
- `sidebar[data-variant="compact"]` réduit la navigation à 72 px uniquement pendant le déploiement de la toolbox latérale : la manette conserve sa rotation au hover, les trois entrées principales et les trois raccourcis secondaires utilisent `Tooltip`, puis l’encart inférieur contient uniquement les actions globales exporter/importer empilées avec leurs propres infobulles ; à la fermeture du panneau, la sidebar desktop reprend sa variante normale.
|
||||
- `sidebar[data-variant="compact"]` réduit la navigation à 72 px jusqu'à `1024px`, ainsi que lorsqu'un panneau toolbox latéral est ouvert en fenêtre large : la manette conserve sa rotation au hover, les trois entrées principales et les trois raccourcis secondaires utilisent `Tooltip`, puis l’encart inférieur contient uniquement les actions globales exporter/importer empilées avec leurs propres infobulles.
|
||||
|
||||
Le nom de marque affiche `Sokko` en blanc et `G` en dore. Au hover, `Sokko` anime vers le dore de droite a gauche puis revient au hover out.
|
||||
|
||||
|
|
@ -623,6 +625,8 @@ Outil Tableau :
|
|||
|
||||
Les toolboxes sont locales et stockees dans le navigateur.
|
||||
|
||||
La fenêtre « Ajouter un outil » peut s'étendre jusqu'à `900px` et présente les outils sur 4 colonnes à `768px`, puis 5 à partir de `900px`. Chaque carte compacte place l'icône à gauche du nom complet, puis une courte phrase d'usage issue de `site.json` sur toute la largeur. Le contenu est réparti dans les cinq catégories du catalogue, chacune signalée par un titre et un séparateur discret, sans onglets de filtrage. Lorsque la hauteur disponible ne suffit pas, la modale utilise directement `scrollbar-control`, la scrollbar verticale du NDS. L'ajout rapide reste une grille d'icônes sans description ; ses boutons utilisent les infobulles NDS au survol et au focus.
|
||||
|
||||
Regles UI :
|
||||
|
||||
- pas de champ description ;
|
||||
|
|
@ -668,7 +672,7 @@ Drawer toolbox :
|
|||
- 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.
|
||||
|
||||
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.
|
||||
La cible responsive est une fenêtre d'ordinateur de second écran, en plein écran ou en demi-écran. Les parcours sont validés à partir de `768px`. Il ne faut pas concevoir de navigation, de composant ni de parcours dédié au téléphone. Sous la largeur validée, les règles servent uniquement à conserver l'accès au contenu et à empêcher un débordement horizontal global, 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.
|
||||
|
||||
|
|
@ -680,7 +684,8 @@ Notifications internes :
|
|||
- une nouvelle alerte s'empile au-dessus des precedentes ;
|
||||
- limite visible : 5 toasts ;
|
||||
- a la 6e alerte, fermer la plus ancienne ;
|
||||
- duree d'affichage cible : 2 minutes, fermeture manuelle possible ;
|
||||
- durée par défaut des confirmations ordinaires : 6 secondes ; une durée explicite reste prioritaire avec un minimum d'une seconde ;
|
||||
- les alertes Timer, Calendrier et le rappel de sauvegarde sont persistants jusqu'à leur fermeture manuelle ;
|
||||
- style plus visible que les confirmations simples : bordure conique animee, pulse leger, arrivee depuis le cote configure.
|
||||
|
||||
## 17. Controles Interactifs
|
||||
|
|
@ -861,7 +866,7 @@ Date picker et menus compacts :
|
|||
|
||||
- afficher les portraits dans un cadre compact 3 / 4 plafonné à 250 px de hauteur, avec `object-fit: cover` et cadrage centré ; les fichiers source restent inchangés ;
|
||||
- les deux variantes de Petit Esprit de Feu utilisent `object-position: center 20%` pour descendre légèrement le personnage dans son cadre ;
|
||||
- la grille utilise un minimum de 220 px pour afficher cinq colonnes à 1920 px, puis réduit le nombre de colonnes sur les petits écrans ; limiter les cartes à 280 px de largeur ; le recto fixe leur hauteur avec le portrait et un pied de 96 px, indépendamment des noms et des variantes ;
|
||||
- la grille utilise un minimum de 220 px pour afficher cinq colonnes à 1920 px, puis réduit le nombre de colonnes selon la largeur disponible ; limiter les cartes à 280 px de largeur ; le recto fixe leur hauteur avec le portrait et un pied de 96 px, indépendamment des noms et des variantes ;
|
||||
- le nom de forme revient à la ligne au recto, dans le pied fixe de 96 px ;
|
||||
- le recto affiche uniquement les icônes d'éléments, centrées dans le pied de carte, avec texte alternatif et infobulle ;
|
||||
- le numéro Aniimo reste un texte HTML visible en bas à gauche du portrait, afin de rester accessible à la recherche du navigateur ;
|
||||
|
|
@ -903,7 +908,9 @@ masquage. La colonne d'actions occupe la hauteur existante du
|
|||
header afin que cet encart n'ajoute pas de ligne ou d'espace vertical. Cette
|
||||
association est conservée dans `localStorage` entre les sessions ; elle est retirée
|
||||
si la checklist, la toolbox liée ou la liaison jeu/toolbox disparaît.
|
||||
Le bouton checklist des pages de résultats filtrables ouvre une modale NDS qui
|
||||
Le bouton texte « Utiliser les résultats », accompagné de l'icône de copie, est visible
|
||||
dans l'en-tête des pages de résultats filtrables. Son `aria-label` et son infobulle
|
||||
conservent le contexte précis des éléments concernés. Il ouvre une modale NDS qui
|
||||
rappelle que les filtres, le regroupement et le masquage déterminent les éléments
|
||||
exportés. La copie texte reste disponible sans toolbox liée ; l'action de création
|
||||
est alors désactivée et accompagnée d'une notice inline. Avec une toolbox liée, la
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ Webapp React locale pour préparer et accompagner des sessions gaming sur second
|
|||
|
||||
Sokko G propose des toolboxes modulaires stockées dans le navigateur, ainsi que des pages de guides de jeu maintenues dans le dépôt. Aucune donnée utilisateur n’est envoyée côté serveur.
|
||||
|
||||
L'interface cible les fenêtres d'ordinateur en plein écran ou en demi-écran. Les mises en page sont validées à partir de 768 px ; le projet ne prévoit pas d'interface ni de navigation dédiées au téléphone.
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
- Toolboxes locales stockées dans IndexedDB.
|
||||
|
|
@ -11,7 +13,7 @@ Sokko G propose des toolboxes modulaires stockées dans le navigateur, ainsi que
|
|||
- Import/export d’une toolbox.
|
||||
- Import/export global de toutes les toolboxes.
|
||||
- Association locale entre une page jeu et une toolbox.
|
||||
- Panneau latéral redimensionnable sur les pages jeux.
|
||||
- Bascule Guide / Toolbox sous 1440 px et panneau latéral redimensionnable à partir de 1440 px sur les pages jeux.
|
||||
- Suivi d’un budget de stockage recommandé, avec quota navigateur en information.
|
||||
- Pages de guides de jeu éditables via JSON.
|
||||
- Listes de jeu exportables vers les checklists.
|
||||
|
|
|
|||
|
|
@ -43,14 +43,14 @@ Les efforts sont relatifs, pas des engagements de délai : faible = changement l
|
|||
| 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 | Donner une explication à chaque outil | Mis en œuvre : les 17 choix affichent leur nom complet et une phrase d'usage. | Réalisé |
|
||||
| P1 | Faire de la création la première action | Mis en œuvre : l'action du hero crée la première toolbox, puis mène à la liste. | Réalisé |
|
||||
| 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 | Rendre visible la création de checklist depuis un guide | Mis en œuvre : « Utiliser les résultats » remplace l'icône seule sur les listes filtrables. | Réalisé |
|
||||
| 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 |
|
||||
| P2 | Alléger les barres d'actions et notifications | Partiellement réalisé : confirmations à 6 secondes, alertes Timer, Calendrier et sauvegarde persistantes. | 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
|
||||
|
|
@ -65,7 +65,7 @@ Les efforts sont relatifs, pas des engagements de délai : faible = changement l
|
|||
|
||||
### 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.
|
||||
**État actuel.** Sans toolbox, le premier bloc affiche désormais « Créer ma première toolbox » et ouvre la création avec le tutoriel activé. Dès qu'une toolbox existe, l'action devient « Voir mes toolboxes ». Le bouton placé après les étapes reste disponible comme rappel.
|
||||
|
||||
**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. »
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ Réduire les compteurs « 0 toolbox / 17 outils » à une information secondaire
|
|||
|
||||
### 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.
|
||||
**État actuel.** La fenêtre d'ajout s'étend jusqu'à 900 px et présente les 17 icônes, les noms complets et une courte phrase d'usage. Elle affiche quatre ou cinq colonnes selon la largeur et répartit directement les outils dans les cinq sections du catalogue, sans onglets supplémentaires. Le panneau NDS gère le défilement vertical. L'ajout rapide au chevron reste inchangé dans son principe et utilise les infobulles NDS.
|
||||
|
||||
**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.
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ Ajouter recherche et favoris lorsque la collection grandit. Un tri « Dernière
|
|||
|
||||
**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.
|
||||
**Guides de jeu.** L'action visible « Utiliser les résultats » ouvre désormais la modale de copie ou de création de checklist, tout en gardant un libellé accessible contextuel. La destination liée et l'absence de toolbox sont expliquées dans la modale. Le choix ou la création d'une toolbox dans ce même parcours reste une amélioration possible.
|
||||
|
||||
**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.
|
||||
|
||||
|
|
@ -141,7 +141,7 @@ Dans la fenêtre de sauvegarde automatique, mettre d'abord l'état, le dossier e
|
|||
|
||||
**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.
|
||||
Réserver l'accent le plus fort à l'action principale du bloc. Rendre les bandes de mémos facultatives au démarrage. Les confirmations ordinaires disparaissent désormais après 6 secondes ; les alertes Timer, Calendrier et sauvegarde restent affichées jusqu'à leur fermeture manuelle.
|
||||
|
||||
### 9. Corriger les interactions clavier des fenêtres
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
|
||||
Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page jeu ou d'un format de données.
|
||||
|
||||
## Cible d'affichage
|
||||
|
||||
- Concevoir et valider les fonctionnalités pour une fenêtre d'ordinateur en plein écran ou en demi-écran, à partir de `768px`.
|
||||
- Ne pas ajouter de navigation, de composant, de libellé ou de parcours réservé au téléphone.
|
||||
- Sous `768px`, vérifier uniquement que le contenu reste accessible, que la sidebar demeure utilisable et qu'aucun débordement horizontal global n'est introduit.
|
||||
- Conserver les adaptations internes nécessaires aux composants étroits, notamment les formulaires, tableaux et outils, sans en faire une interface mobile distincte.
|
||||
|
||||
## Outil Toolbox
|
||||
|
||||
- Ajouter ou modifier le composant dans `website/src/features/toolboxes/modules/`.
|
||||
|
|
@ -10,6 +17,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
|||
- Dans la Librairie, vérifier les liens directs vers un outil, les changements d'ancre et le retour navigateur ; une nouvelle sélection ou une sortie de page doit annuler les défilements différés précédents.
|
||||
- Ajouter les textes nécessaires dans `website/public/data/site.json`.
|
||||
- Mettre à jour la validation de `site.json` dans `tests/helpers/data-validation.mjs`.
|
||||
- Pour chaque type du registre, fournir une description non vide dans `toolboxes.addToolDescriptions` et vérifier que la fenêtre « Ajouter un outil » affiche le nom complet et la courte phrase d'usage aux largeurs compactes validées. Vérifier sa largeur réelle jusqu'à `900px`, les cartes compactes avec icône à gauche du titre, les 4 et 5 colonnes selon la largeur, les cinq sections sans onglets, les couleurs et dimensions de `scrollbar-control`, ainsi que les infobulles de l'ajout rapide au survol comme au focus.
|
||||
- Mettre à jour la normalisation et le stockage compact dans `website/src/features/toolboxes/storage/modules/<outil>.js`, puis vérifier l'export public dans `website/src/features/toolboxes/storage/toolboxStorage.js`.
|
||||
- Mettre à jour `docs/STORAGE_SCHEMA.md`.
|
||||
- Vérifier l'import et l'export si l'outil stocke des données.
|
||||
|
|
@ -38,6 +46,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
|||
- Pour Mémos, vérifier l'édition courte, les couleurs, la réorganisation et la bande déroulante ; vérifier aussi les liaisons Aucun / Tous / planificateur précis, les alertes de tâches proches du reset et le retour à Aucun après suppression du dernier planificateur lié.
|
||||
- Les parcours `e2e/calculator.spec.js` vérifient la validation des formules, l'édition parent/enfant avec recalcul et persistance, la suppression d'une branche active, le retour à un calcul racine, la réinitialisation du brouillon sans effacer les résultats, la copie pour checklist et la persistance du défilement. La réorganisation des racines doit conserver leurs descendants et refuser de changer le parent d'un enfant.
|
||||
- Vérifier qu'une notification ou une mise à jour sans rapport ne remplace pas un brouillon Mémos avant sa validation au blur.
|
||||
- Vérifier que les confirmations ordinaires disparaissent après 6 secondes, que les durées explicites sont respectées et que les alertes Timer, Calendrier et sauvegarde restent présentes jusqu'à leur fermeture manuelle. La pile reste limitée à cinq alertes.
|
||||
- Pour l'arbre d'objectifs, vérifier la sélection après suppression/import et le retour à Base quand le nœud sélectionné devient une étape sans onglet Contenu.
|
||||
- Vérifier le mode une colonne et deux colonnes.
|
||||
- Vérifier que l'ajout, la suppression et la réorganisation respectent les `moduleOrder.one` et `moduleOrder.two` de l'espace actif.
|
||||
|
|
@ -55,13 +64,13 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
|||
- Vérifier la page `/games`.
|
||||
- Vérifier la page `/games/<gameId>`.
|
||||
- Vérifier le chargement à la première visite, la conservation des données pendant la navigation et les erreurs réseau ; sous StrictMode, les requêtes abandonnées ne doivent pas remplacer les données actuelles ni afficher d'erreur d'annulation.
|
||||
- Vérifier que le bouton texte « Afficher la toolbox » devient « Masquer la toolbox » avec le panneau latéral ouvert.
|
||||
- Sous `1440px`, vérifier la bascule « Guide / Toolbox », la conservation des deux vues après leur première activation et la restauration de leur défilement. À partir de `1440px`, vérifier que le bouton « Afficher la toolbox » devient « Masquer la toolbox » avec le panneau latéral ouvert.
|
||||
- Sans toolbox liée, vérifier la création ainsi que l'association directe d'une toolbox existante ; le sélecteur ne doit proposer que les toolboxes qui ne sont liées à aucun jeu.
|
||||
- Vérifier le breadcrumb.
|
||||
- Vérifier les textes avec retours à la ligne.
|
||||
- Pour les guides à cartes, vérifier le toggle de lien à côté du « + » dans l'en-tête de chaque checklist compatible : il affiche les commandes, masquées par défaut. Vérifier l'association exclusive, le masquage désactivé sur les autres listes et la mise à jour immédiate depuis le panneau latéral. Refermer les commandes conserve l'association. La checkbox des cartes met à jour toutes les lignes exactes correspondantes, respecte les quantités cibles et ne retourne pas les cartes.
|
||||
- Vérifier le changement de checklist, la dissociation, la restauration de session, la suppression d'un module et le changement de toolbox. La réinitialisation des filtres conserve la liaison ; une liste devenue sans correspondance reste sélectionnée avec un message.
|
||||
- Pour les résultats filtrables Aniimo, MHW et Diablo, vérifier que l'action checklist ouvre une modale rappelant que seuls les éléments visibles sont utilisés. La copie reste disponible sans toolbox liée ; la création est alors désactivée avec une indication explicite. Avec une toolbox liée, la création ajoute une nouvelle checklist dans son espace actif sans remplacer l'association guide/checklist existante.
|
||||
- Pour les résultats filtrables Aniimo, MHW et Diablo, vérifier que le bouton visible « Utiliser les résultats » conserve un libellé accessible contextuel et ouvre une modale rappelant que seuls les éléments visibles sont utilisés. Le groupe titre, compteur et action doit revenir à la ligne sans débordement dès 768 px. La copie reste disponible sans toolbox liée ; la création est alors désactivée avec une indication explicite. Avec une toolbox liée, la création ajoute une nouvelle checklist dans son espace actif sans remplacer l'association guide/checklist existante.
|
||||
- Pour Aniimo, vérifier la copie et la création des espèces ou des formes selon l'affichage, les noms français/anglais, la priorité des formes et le masquage avant regroupement. Compteurs, copie et création doivent correspondre aux résultats affichés.
|
||||
|
||||
## Listes de jeu
|
||||
|
|
@ -81,7 +90,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 cartes, panels et scrollbars.
|
||||
- Vérifier les états hover/focus.
|
||||
- Vérifier les fenêtres de 768, 960, 1024, 1280, 1440 et 1920 px 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, avec une hauteur de 720 px pour les parcours compacts.
|
||||
- Effectuer seulement un contrôle minimal sous 768 px : accès au contenu, sidebar utilisable et absence de débordement horizontal global.
|
||||
- Sur l'accueil vide, vérifier que l'action principale ouvre la création avec tutoriel ; avec au moins une toolbox, vérifier qu'elle devient « Voir mes toolboxes ». L'action « Voir les jeux » et le rappel de création en fin de prise en main restent disponibles.
|
||||
- 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 fenêtre large/compacte : fonds,
|
||||
bordures, séparateurs dorés, hover sans redimensionnement, focus clavier NDS
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ async function setup(page) {
|
|||
await dialog.getByLabel("Nom").fill("Calculator workflows");
|
||||
await dialog.getByRole("button", { name: "Créer", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Ajouter un outil", exact: true }).click();
|
||||
await page.getByRole("dialog", { name: "Ajouter un outil" }).getByRole("button", { name: "Calculateur", exact: true }).click();
|
||||
await page.getByRole("dialog", { name: "Ajouter un outil" }).getByRole("button", { name: "Calculateur" }).click();
|
||||
return page.locator('[data-module-type="calculator"]');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,11 +18,14 @@ async function setup(page) {
|
|||
context.fillRect(0, 0, 32, 32);
|
||||
const green = canvas.toDataURL();
|
||||
const targetAt = Date.now() + 60000;
|
||||
const currentDate = new Date();
|
||||
const currentDateKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, "0")}-${String(currentDate.getDate()).padStart(2, "0")}`;
|
||||
const toolboxes = [
|
||||
{ id: "a", name: "Alpha", updatedAt: "2026-09-12T12:00:00Z", modules: [
|
||||
{ id: "images", type: "images", title: "Images" },
|
||||
{ id: "map", type: "imageAnnotation", title: "Carte" },
|
||||
{ id: "timer", type: "timer", title: "Timer" }
|
||||
{ id: "timer", type: "timer", title: "Timer" },
|
||||
{ id: "calendar", type: "calendar", title: "Calendrier" }
|
||||
] },
|
||||
{ id: "b", name: "Beta", updatedAt: "2026-09-12T12:00:00Z", modules: [{ id: "images", type: "images", title: "Images" }] }
|
||||
];
|
||||
|
|
@ -30,6 +33,7 @@ async function setup(page) {
|
|||
"a:images": { images: [{ id: "red", label: "Rouge", dataUrl: red }] },
|
||||
"a:map": { image: green, markers: [{ id: "marker", x: 25, y: 50, label: "Entrée" }], drawings: { strokes: [{ id: "stroke", color: "#f6c453", width: 4, points: [{ x: 10, y: 10 }, { x: 80, y: 80 }] }] } },
|
||||
"a:timer": { countdowns: [{ id: "alert", type: "duration", label: "Alerte fermée", targetAt, durationMs: 60000, alertMode: "site" }] },
|
||||
"a:calendar": { viewMode: "month", events: [{ id: "current", title: "Événement persistant", startDate: currentDateKey, endDate: currentDateKey, repeat: "none", alertMode: "site", alertOffsetMinutes: [0] }] },
|
||||
"b:images": { images: [{ id: "green", label: "Vert", dataUrl: green }] }
|
||||
};
|
||||
await db.setToolboxSnapshot(toolboxes, { mhwilds: "a" }, modules);
|
||||
|
|
@ -71,12 +75,16 @@ async function exportJson(page, buttonName = "Exporter tout") {
|
|||
test("closed media stays unread while site-wide timer alerts still fire", async ({ page }) => {
|
||||
const seeded = await setup(page);
|
||||
expect(await page.evaluate(() => window.moduleReads)).not.toEqual([]);
|
||||
expect(await page.evaluate(() => window.moduleReads.every((key) => key === "a:timer"))).toBe(true);
|
||||
expect(await page.evaluate(() => window.moduleReads.every((key) => ["a:timer", "a:calendar"].includes(key)))).toBe(true);
|
||||
await expect(page.getByRole("status").filter({ hasText: "Événement persistant" })).toBeVisible();
|
||||
await page.clock.install({ time: new Date(seeded.targetAt - 3000) });
|
||||
await page.clock.pauseAt(new Date(seeded.targetAt - 2000));
|
||||
await page.clock.runFor(3000);
|
||||
await expect(page.getByRole("status").filter({ hasText: "Alerte fermée" })).toBeVisible();
|
||||
expect(await page.evaluate(() => window.moduleReads.every((key) => key === "a:timer"))).toBe(true);
|
||||
await page.clock.runFor(7000);
|
||||
await expect(page.getByRole("status").filter({ hasText: "Alerte fermée" })).toBeVisible();
|
||||
await expect(page.getByRole("status").filter({ hasText: "Événement persistant" })).toBeVisible();
|
||||
expect(await page.evaluate(() => window.moduleReads.every((key) => ["a:timer", "a:calendar"].includes(key)))).toBe(true);
|
||||
});
|
||||
|
||||
test("creation, import and deletion preserve unread media and global exports include it", async ({ page }) => {
|
||||
|
|
@ -86,14 +94,14 @@ test("creation, import and deletion preserve unread media and global exports inc
|
|||
await page.getByRole("button", { name: "Créer", exact: true }).click();
|
||||
await expect.poll(async () => (await disk(page)).toolboxes.length).toBe(3);
|
||||
expect((await disk(page)).modules).toMatchObject(seeded.modules);
|
||||
expect(await page.evaluate(() => window.moduleReads.every((key) => key === "a:timer"))).toBe(true);
|
||||
expect(await page.evaluate(() => window.moduleReads.every((key) => ["a:timer", "a:calendar"].includes(key)))).toBe(true);
|
||||
await page.goto("/#/toolboxes");
|
||||
await page.getByText("Importer une toolbox", { exact: true }).locator("input").setInputFiles({
|
||||
name: "import.json", mimeType: "application/json", buffer: Buffer.from(JSON.stringify({ toolbox: { id: "source", name: "Imported", modules: [] }, modules: {} }))
|
||||
});
|
||||
await expect(page.locator(".toolbox-card")).toHaveCount(4);
|
||||
expect((await disk(page)).modules).toMatchObject(seeded.modules);
|
||||
expect(await page.evaluate(() => window.moduleReads.every((key) => key === "a:timer"))).toBe(true);
|
||||
expect(await page.evaluate(() => window.moduleReads.every((key) => ["a:timer", "a:calendar"].includes(key)))).toBe(true);
|
||||
const exported = await exportJson(page);
|
||||
for (const key of ["a:images", "a:map", "b:images"]) expect(exported.modules[key]).toEqual(seeded.modules[key]);
|
||||
await page.getByRole("button", { name: "Supprimer Beta", exact: true }).click();
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ test("Aniimo copies visible species or forms and round-trips through checklist i
|
|||
await expect(page.locator(".guide-checklist-controls")).toHaveCount(0);
|
||||
const copy = page.locator(".results-copy-button");
|
||||
await expect(copy).toHaveAccessibleName(/sans les variantes/);
|
||||
await expect(copy).toContainText("Utiliser les résultats");
|
||||
await expect(page.locator('[data-copy-target="true"]')).toHaveText("1 / 88 Aniimos");
|
||||
await copyVisibleResults(page);
|
||||
await expect.poll(() => page.evaluate(() => globalThis.navigator.clipboard.readText())).toBe("Loufeuteau:1");
|
||||
|
|
@ -319,7 +320,9 @@ for (const [gameId, category, cardClass, dataPath, dataKey, checklistTitle] of [
|
|||
await seed(page, gameId, { progress: [row(label, 1)] });
|
||||
await page.goto(`/#/games/${gameId}/${category}`);
|
||||
await expect(page.locator(cardClass).first()).toBeVisible();
|
||||
await page.locator(".results-copy-button").click();
|
||||
const resultsAction = page.locator(".results-copy-button");
|
||||
await expect(resultsAction).toContainText("Utiliser les résultats");
|
||||
await resultsAction.click();
|
||||
const resultsDialog = page.getByRole("dialog", { name: "Utiliser les résultats visibles" });
|
||||
await expect(resultsDialog.getByText(checklistTitle, { exact: true })).toBeVisible();
|
||||
await resultsDialog.getByRole("button", { name: "Fermer", exact: true }).click();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,36 @@
|
|||
// Rôle : vérifie en navigateur que la home charge les zones principales.
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { resetSokkoStorage } from "./helpers/storage.js";
|
||||
|
||||
test.beforeEach(async ({ page }) => resetSokkoStorage(page));
|
||||
|
||||
test("home page renders the application shell", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.locator(".app-shell")).toBeVisible();
|
||||
const main = page.getByRole("main");
|
||||
const hero = main.locator(".hero");
|
||||
await expect(main).toBeVisible();
|
||||
await expect(main.getByRole("heading", { name: "Sokko G", exact: true })).toBeVisible();
|
||||
await expect(main.getByRole("link", { name: "Voir les toolboxes", exact: true })).toBeVisible();
|
||||
const create = hero.getByRole("button", { name: "Créer ma première toolbox", exact: true });
|
||||
await expect(create).toBeVisible();
|
||||
await expect(main.getByRole("link", { name: "Voir les jeux", exact: true })).toBeVisible();
|
||||
await expect(page.locator(".home-stats")).toBeVisible();
|
||||
await expect(page.locator(".origin-section")).toBeVisible();
|
||||
|
||||
await create.click();
|
||||
await expect(page.getByRole("dialog", { name: "Nouvelle toolbox", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("home links to the toolbox list once a toolbox exists", async ({ page }) => {
|
||||
await page.locator(".hero").getByRole("button", { name: "Créer ma première toolbox", exact: true }).click();
|
||||
const dialog = page.getByRole("dialog", { name: "Nouvelle toolbox", exact: true });
|
||||
await dialog.getByRole("textbox", { name: "Nom", exact: true }).fill("Accueil contextualisé");
|
||||
await dialog.getByRole("button", { name: "Créer", exact: true }).click();
|
||||
await page.goto("/#/");
|
||||
|
||||
const link = page.getByRole("main").getByRole("link", { name: "Voir mes toolboxes", exact: true });
|
||||
await expect(link).toBeVisible();
|
||||
await link.click();
|
||||
await expect(page).toHaveURL(/#\/toolboxes$/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ test("reduced motion keeps previews still but manual selection remains available
|
|||
|
||||
test("home onboarding creates the initial tools and starts the tutorial", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByRole("button", { name: content.primaryAction, exact: true }).click();
|
||||
const section = page.getByRole("region", { name: content.title, exact: true });
|
||||
await section.getByRole("button", { name: content.primaryAction, exact: true }).click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
await dialog.getByRole("textbox", { name: "Nom", exact: true }).fill("Prise en main");
|
||||
await dialog.getByRole("button", { name: "Créer", exact: true }).click();
|
||||
|
|
|
|||
|
|
@ -21,26 +21,22 @@ async function setup(page, probe = false) {
|
|||
await page.clock.pauseAt(new Date("2026-09-12T12:00:01Z"));
|
||||
}
|
||||
|
||||
async function notify(page, message, durationMs) {
|
||||
await page.evaluate((detail) => window.dispatchEvent(new CustomEvent("sokkog:notify", { detail })), { message, durationMs });
|
||||
async function notify(page, message, options = {}) {
|
||||
await page.evaluate((detail) => window.dispatchEvent(new CustomEvent("sokkog:notify", { detail })), { message, ...options });
|
||||
await expect(page.getByRole("status").filter({ hasText: message })).toBeVisible();
|
||||
}
|
||||
|
||||
test("application alerts use short clipboard and default durations and dismiss individually", async ({ page }) => {
|
||||
test("ordinary alerts last six seconds while explicit and persistent durations are respected", async ({ page }) => {
|
||||
await setup(page);
|
||||
await notify(page, "Texte copié");
|
||||
await notify(page, "Sauvegarde terminée");
|
||||
await page.clock.runFor(4999);
|
||||
await page.clock.runFor(5999);
|
||||
await expect(page.getByRole("status")).toHaveCount(2);
|
||||
await page.clock.runFor(1);
|
||||
await expect(page.getByRole("status")).toHaveText("Sauvegarde terminée");
|
||||
await page.clock.runFor(114999);
|
||||
await expect(page.getByRole("status")).toHaveCount(1);
|
||||
await page.clock.runFor(1);
|
||||
await expect(page.getByRole("status")).toHaveCount(0);
|
||||
|
||||
await notify(page, "Minimum", 0);
|
||||
await notify(page, "Personnalisée", 2000);
|
||||
await notify(page, "Minimum", { durationMs: 0 });
|
||||
await notify(page, "Personnalisée", { durationMs: 2000 });
|
||||
await page.clock.runFor(999);
|
||||
await expect(page.getByRole("status")).toHaveCount(2);
|
||||
await page.clock.runFor(1);
|
||||
|
|
@ -49,6 +45,17 @@ test("application alerts use short clipboard and default durations and dismiss i
|
|||
await expect(page.getByRole("status")).toHaveCount(0);
|
||||
await page.clock.runFor(2000);
|
||||
await expect(page.getByRole("status")).toHaveCount(0);
|
||||
|
||||
await notify(page, "Alerte persistante", { persistent: true });
|
||||
await page.clock.runFor(120000);
|
||||
await expect(page.getByRole("status")).toHaveText("Alerte persistante");
|
||||
await page.getByRole("button", { name: "Fermer l'alerte" }).click();
|
||||
await expect(page.getByRole("status")).toHaveCount(0);
|
||||
|
||||
for (let index = 1; index <= 6; index += 1) await notify(page, `Pile ${index}`);
|
||||
await expect(page.getByRole("status")).toHaveCount(5);
|
||||
await expect(page.getByRole("status").filter({ hasText: "Pile 1" })).toHaveCount(0);
|
||||
await expect(page.getByRole("status").filter({ hasText: "Pile 6" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("toast replacement and unmount cancel obsolete expiration callbacks under StrictMode", async ({ page }) => {
|
||||
|
|
@ -60,12 +67,17 @@ test("toast replacement and unmount cancel obsolete expiration callbacks under S
|
|||
expect(await page.evaluate(() => window.shellProbe.closed)).toEqual([]);
|
||||
await page.clock.runFor(1);
|
||||
expect(await page.evaluate(() => window.shellProbe.closed)).toEqual(["new"]);
|
||||
await page.evaluate(() => window.shellProbe.render({ toast: { id: "default", message: "Par défaut" } }));
|
||||
await page.clock.runFor(5999);
|
||||
expect(await page.evaluate(() => window.shellProbe.closed)).toEqual(["new"]);
|
||||
await page.clock.runFor(1);
|
||||
expect(await page.evaluate(() => window.shellProbe.closed)).toEqual(["new", "default"]);
|
||||
await page.evaluate(() => {
|
||||
window.shellProbe.render({ toast: { id: "unmounted", message: "Fermée" } });
|
||||
window.shellProbe.unmount();
|
||||
});
|
||||
await page.clock.runFor(120000);
|
||||
expect(await page.evaluate(() => window.shellProbe.closed)).toEqual(["new"]);
|
||||
expect(await page.evaluate(() => window.shellProbe.closed)).toEqual(["new", "default"]);
|
||||
});
|
||||
|
||||
test("overlapping modals keep scroll locked until the last unmount under StrictMode", async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
// Rôle : couvre les parcours navigateur principaux des toolboxes locales.
|
||||
/* global window, document, navigator, CustomEvent, FileSystemDirectoryHandle */
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resetSokkoStorage } from "./helpers/storage.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const fixturePath = path.join(__dirname, "fixtures", "toolbox-export.json");
|
||||
const site = JSON.parse(readFileSync(path.join(__dirname, "..", "website", "public", "data", "site.json"), "utf8"));
|
||||
|
||||
async function collectBrowserErrors(page) {
|
||||
const errors = [];
|
||||
|
|
@ -63,6 +65,65 @@ test("creates a toolbox and lazy-loads an added tool", async ({ page }) => {
|
|||
await expect(page.locator("[data-module-type='goalTree']")).toBeVisible();
|
||||
});
|
||||
|
||||
test("add-tool modal explains every tool without overflowing compact desktop widths", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await createToolbox(page, "Catalogue des outils");
|
||||
await page.getByRole("button", { name: "Ajouter un outil", exact: true }).click();
|
||||
const dialog = page.getByRole("dialog", { name: "Ajouter un outil", exact: true });
|
||||
const cards = dialog.locator(".tool-add-card");
|
||||
const descriptions = Object.entries(site.toolboxes.addToolDescriptions);
|
||||
await expect(cards).toHaveCount(descriptions.length);
|
||||
for (const [index, [, description]] of descriptions.entries()) {
|
||||
await expect(dialog.getByText(description, { exact: true })).toBeVisible();
|
||||
await expect(cards.nth(index).locator("strong")).not.toHaveCSS("text-overflow", "ellipsis");
|
||||
}
|
||||
|
||||
for (const [width, expectedColumns] of [[768, 4], [960, 5], [1024, 5], [1280, 5]]) {
|
||||
await page.setViewportSize({ width, height: 720 });
|
||||
await expect(dialog).toBeVisible();
|
||||
const layout = await dialog.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const grid = element.querySelector(".tool-add-grid");
|
||||
return {
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
height: rect.height,
|
||||
viewportWidth: window.innerWidth,
|
||||
viewportHeight: window.innerHeight,
|
||||
columns: window.getComputedStyle(grid).gridTemplateColumns.split(" ").length,
|
||||
overflowY: window.getComputedStyle(element).overflowY,
|
||||
scrollbarColor: window.getComputedStyle(element).scrollbarColor,
|
||||
scrollbarWidth: window.getComputedStyle(element).scrollbarWidth,
|
||||
webkitScrollbarWidth: window.getComputedStyle(element, "::-webkit-scrollbar").width,
|
||||
scrolls: element.scrollHeight > element.clientHeight,
|
||||
pageFits: document.documentElement.scrollWidth <= window.innerWidth
|
||||
};
|
||||
});
|
||||
expect(layout.left).toBeGreaterThanOrEqual(0);
|
||||
expect(layout.right).toBeLessThanOrEqual(layout.viewportWidth);
|
||||
expect(layout.right - layout.left).toBeGreaterThanOrEqual(Math.min(900, width - 32) - 2);
|
||||
expect(layout.height).toBeLessThanOrEqual(layout.viewportHeight);
|
||||
expect(layout.columns).toBe(expectedColumns);
|
||||
expect(layout.overflowY).toBe("auto");
|
||||
expect(layout.scrollbarColor).toContain("88, 55, 174");
|
||||
expect(layout.scrollbarWidth).toBe("thin");
|
||||
expect(layout.webkitScrollbarWidth).toBe("6px");
|
||||
expect(layout.scrolls).toBe(true);
|
||||
expect(layout.pageFits).toBe(true);
|
||||
}
|
||||
|
||||
const categorySections = dialog.locator(".tool-add-category-section");
|
||||
await expect(categorySections).toHaveCount(5);
|
||||
await expect(categorySections.locator("h3")).toHaveText(Object.values(site.library.categories));
|
||||
await expect(dialog.locator("[aria-pressed]")).toHaveCount(0);
|
||||
await dialog.getByRole("button", { name: "Fermer", exact: true }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Afficher l'ajout rapide", exact: true }).click();
|
||||
const quickImage = page.getByRole("button", { name: "Ajouter Images", exact: true });
|
||||
await quickImage.focus();
|
||||
await expect(page.getByRole("tooltip", { name: "Images", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("keeps an uncommitted memo through unrelated app updates and saves multiline text", async ({ page }) => {
|
||||
await createToolbox(page, "Memo draft toolbox");
|
||||
const memos = moduleByType(page, "memos");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// Rôle : centralise les règles de validation des JSON éditables du projet.
|
||||
import assert from "node:assert/strict";
|
||||
import { TOOLBOX_MODULE_DEFINITIONS } from "../../website/src/features/toolboxes/toolboxModuleRegistry.js";
|
||||
|
||||
const SNAKE_CASE_RE = /^[a-z0-9]+(?:_[a-z0-9]+)*$/;
|
||||
const PHYSICAL_HITZONE_KEYS = ["cut", "blunt", "ammo"];
|
||||
|
|
@ -101,6 +102,7 @@ export function validateSiteContent(site) {
|
|||
"guideChecklist.completed",
|
||||
"guideChecklist.checkCard",
|
||||
"guideChecklist.uncheckCard",
|
||||
"guideChecklist.resultsActionLabel",
|
||||
"guideChecklist.resultsModalTitle",
|
||||
"guideChecklist.resultsModalDescription",
|
||||
"guideChecklist.resultsChecklistLabel",
|
||||
|
|
@ -184,6 +186,7 @@ export function validateSiteContent(site) {
|
|||
"home.hero.title",
|
||||
"home.hero.description",
|
||||
"home.hero.primaryAction",
|
||||
"home.hero.createAction",
|
||||
"home.hero.secondaryAction",
|
||||
"home.stats.toolboxSingular",
|
||||
"home.stats.toolboxPlural",
|
||||
|
|
@ -985,6 +988,12 @@ export function validateSiteContent(site) {
|
|||
"toolboxes.emptyText"
|
||||
].forEach((path) => assertNonEmptyString(site, path));
|
||||
|
||||
const descriptions = valueAt(site, "toolboxes.addToolDescriptions");
|
||||
const moduleTypes = Object.keys(TOOLBOX_MODULE_DEFINITIONS).sort();
|
||||
assert.ok(descriptions && typeof descriptions === "object" && !Array.isArray(descriptions), "toolboxes.addToolDescriptions must be an object");
|
||||
assert.deepEqual(Object.keys(descriptions).sort(), moduleTypes, "toolboxes.addToolDescriptions keys must exactly match the toolbox module registry");
|
||||
moduleTypes.forEach((type) => assertNonEmptyString(site, `toolboxes.addToolDescriptions.${type}`));
|
||||
|
||||
assertStringArray(site, "toolboxes.storageHelp.items", 3);
|
||||
assertStringArray(site, "toolboxes.autoBackup.browsers", 2);
|
||||
assertStringArray(site, "toolboxes.autoBackup.permissionSteps", 3);
|
||||
|
|
|
|||
|
|
@ -1553,7 +1553,8 @@
|
|||
"eyebrow": "Session gaming efficace",
|
||||
"title": "Sokko G",
|
||||
"description": "Centralisez vos outils et repères de jeu dans une interface locale, rapide à consulter, pensée pour accompagner vos sessions sans interrompre l’action.",
|
||||
"primaryAction": "Voir les toolboxes",
|
||||
"primaryAction": "Voir mes toolboxes",
|
||||
"createAction": "Créer ma première toolbox",
|
||||
"secondaryAction": "Voir les jeux"
|
||||
},
|
||||
"stats": {
|
||||
|
|
@ -1856,6 +1857,25 @@
|
|||
"eyebrow": "Données locales",
|
||||
"title": "Toolboxes",
|
||||
"newButton": "Nouvelle toolbox",
|
||||
"addToolDescriptions": {
|
||||
"notepad": "Rédiger des notes sur plusieurs pages.",
|
||||
"memos": "Garder de courts rappels sous les yeux.",
|
||||
"checklist": "Suivre des objectifs et leurs quantités.",
|
||||
"images": "Réunir des images de référence.",
|
||||
"imageAnnotation": "Annoter une image avec des repères.",
|
||||
"links": "Regrouper ses liens utiles.",
|
||||
"counters": "Compter des essais, victoires ou ressources.",
|
||||
"randomizer": "Tirer au sort avec dés, roue ou équipes.",
|
||||
"rngSimulator": "Estimer une probabilité et suivre ses tirages.",
|
||||
"calculator": "Enregistrer des calculs liés.",
|
||||
"table": "Organiser données et formules en grille.",
|
||||
"goalTree": "Décomposer un objectif en étapes.",
|
||||
"timer": "Lancer chronomètres et comptes à rebours.",
|
||||
"calendar": "Planifier événements et rappels.",
|
||||
"taskPlanner": "Suivre des tâches ponctuelles ou récurrentes.",
|
||||
"combos": "Mémoriser des séquences de commandes.",
|
||||
"equipmentPlanner": "Préparer un build et ses équipements."
|
||||
},
|
||||
"libraryLink": "Voir la librairie d'outils",
|
||||
"importAll": "Importer tout",
|
||||
"exportAll": "Exporter tout",
|
||||
|
|
@ -2844,6 +2864,7 @@
|
|||
"completed": "Coché",
|
||||
"checkCard": "Cocher dans la checklist",
|
||||
"uncheckCard": "Décocher dans la checklist",
|
||||
"resultsActionLabel": "Utiliser les résultats",
|
||||
"resultsModalTitle": "Utiliser les résultats visibles",
|
||||
"resultsModalDescription": "Seuls les éléments actuellement visibles sur la page seront utilisés, après application des filtres, du regroupement et du masquage des éléments cochés.",
|
||||
"resultsChecklistLabel": "Nouvelle checklist",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export function ConfirmModal({ title, message, confirmLabel = "Confirmer", cance
|
|||
);
|
||||
}
|
||||
|
||||
export function NotificationToast({ id, message, durationMs = 120000, actionLabel = "", onAction, onClose }) {
|
||||
export function NotificationToast({ id, message, durationMs = 6000, actionLabel = "", onAction, onClose }) {
|
||||
useEffect(() => {
|
||||
if (durationMs <= 0) return undefined;
|
||||
const timeoutId = window.setTimeout(() => onClose(id), durationMs);
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ export function CopyChecklistItemsButton({
|
|||
return (
|
||||
<>
|
||||
<button
|
||||
className="filter-reset-button results-copy-button nds-icon-button"
|
||||
className={filteredResults ? "results-copy-button nds-button" : "filter-reset-button results-copy-button nds-icon-button"}
|
||||
data-size="xs"
|
||||
type="button"
|
||||
onClick={filteredResults ? () => setOpen(true) : copyItems}
|
||||
|
|
@ -174,6 +174,7 @@ export function CopyChecklistItemsButton({
|
|||
title={copied ? "Copié" : title}
|
||||
>
|
||||
<Icon name="copy" />
|
||||
{filteredResults && <span>{content.resultsActionLabel || "Utiliser les résultats"}</span>}
|
||||
</button>
|
||||
{filteredResults && open && (
|
||||
<ChecklistResultsModal
|
||||
|
|
|
|||
|
|
@ -360,7 +360,11 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
)}
|
||||
</div>
|
||||
<div className="actions nds-toolbar">
|
||||
<AddToolControls onAdd={addModule} />
|
||||
<AddToolControls
|
||||
categories={siteContent.library.categories}
|
||||
descriptions={siteContent.toolboxes.addToolDescriptions}
|
||||
onAdd={addModule}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<ToolboxSpaces
|
||||
|
|
|
|||
|
|
@ -3,12 +3,17 @@ import { useEffect, useState } from "react";
|
|||
import { createPortal } from "react-dom";
|
||||
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { Tooltip } from "../../../components/Tooltip.jsx";
|
||||
import { lockBodyScroll } from "../../../utils/bodyScrollLock.js";
|
||||
import { TOOLBOX_MODULES } from "../toolboxModuleRegistry.js";
|
||||
import { LIBRARY_CATEGORY_DEFINITIONS, TOOLBOX_MODULES } from "../toolboxModuleRegistry.js";
|
||||
|
||||
export function AddToolControls({ onAdd }) {
|
||||
export function AddToolControls({ categories, descriptions, onAdd }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const modules = Object.entries(TOOLBOX_MODULES);
|
||||
const moduleCategories = LIBRARY_CATEGORY_DEFINITIONS.map((category) => ({
|
||||
...category,
|
||||
title: categories[category.key]
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
|
|
@ -20,6 +25,19 @@ export function AddToolControls({ onAdd }) {
|
|||
setOpen(false);
|
||||
}
|
||||
|
||||
function renderToolCard(type) {
|
||||
const module = TOOLBOX_MODULES[type];
|
||||
return (
|
||||
<button key={type} className="tool-add-card" type="button" onClick={() => addTool(type)}>
|
||||
<span className="module-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${module.icon}`} />
|
||||
</span>
|
||||
<strong>{module.label}</strong>
|
||||
<span>{descriptions[type]}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
@ -43,28 +61,31 @@ export function AddToolControls({ onAdd }) {
|
|||
menuClassName="tool-quick-add"
|
||||
label="Ajout rapide"
|
||||
renderTrigger={({ open: quickOpen, toggle }) => (
|
||||
<button
|
||||
<Tooltip
|
||||
as="button"
|
||||
className={`tool-add-quick-toggle nds-icon-button ${quickOpen ? "active" : ""}`}
|
||||
content={quickOpen ? "Masquer l'ajout rapide" : "Afficher l'ajout rapide"}
|
||||
data-size="sm"
|
||||
data-variant="primary"
|
||||
data-edge="right"
|
||||
data-border="joined"
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
aria-label="Afficher l'ajout rapide"
|
||||
aria-label={quickOpen ? "Masquer l'ajout rapide" : "Afficher l'ajout rapide"}
|
||||
aria-expanded={quickOpen}
|
||||
title="Ajout rapide"
|
||||
>
|
||||
<Icon name="dropdown" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
{({ close }) => (
|
||||
<>
|
||||
{modules.map(([type, module]) => (
|
||||
<button
|
||||
<Tooltip
|
||||
as="button"
|
||||
key={type}
|
||||
className="tool-quick-add-button nds-icon-button"
|
||||
content={module.label}
|
||||
data-size="sm"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
|
|
@ -72,35 +93,32 @@ export function AddToolControls({ onAdd }) {
|
|||
close();
|
||||
}}
|
||||
aria-label={`Ajouter ${module.label}`}
|
||||
title={module.label}
|
||||
>
|
||||
<span className="module-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${module.icon}`} />
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</CompactDropdown>
|
||||
</div>
|
||||
{open && createPortal(
|
||||
<div className="tool-add-modal-root" role="dialog" aria-modal="true" aria-labelledby="tool-add-modal-title">
|
||||
<div className="tool-add-modal-root">
|
||||
<div className="confirm-backdrop nds-modal-backdrop" onClick={() => setOpen(false)} />
|
||||
<section className="confirm-modal nds-modal tool-add-modal">
|
||||
<section className="confirm-modal nds-modal tool-add-modal" role="dialog" aria-modal="true" aria-labelledby="tool-add-modal-title">
|
||||
<header>
|
||||
<h2 id="tool-add-modal-title">Ajouter un outil</h2>
|
||||
<button className="drawer-close-button nds-icon-button" data-size="sm" type="button" onClick={() => setOpen(false)} aria-label="Fermer" title="Fermer">
|
||||
<Icon name="close" />
|
||||
</button>
|
||||
</header>
|
||||
<div className="tool-add-grid">
|
||||
{modules.map(([type, module]) => (
|
||||
<button key={type} className="tool-add-card" type="button" onClick={() => addTool(type)}>
|
||||
<span className="module-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${module.icon}`} />
|
||||
</span>
|
||||
<span>{module.label}</span>
|
||||
</button>
|
||||
<div className="tool-add-category-list">
|
||||
{moduleCategories.map((category) => (
|
||||
<section className="tool-add-category-section" key={category.key} aria-labelledby={`tool-add-category-${category.key}`}>
|
||||
<h3 id={`tool-add-category-${category.key}`}><span>{category.title}</span></h3>
|
||||
<div className="tool-add-grid">{category.types.map(renderToolCard)}</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export function useToolboxAlerts({ toolboxes, moduleData, links, route, drawerGa
|
|||
nextTargets.set(key, targetMs);
|
||||
|
||||
if (!targetMs || previousTargetMs <= lastTickMs || previousTargetMs > nowMs) return;
|
||||
notify(formatAlertMessage(timerAlertTemplate, toolbox, countdown.label));
|
||||
notify(formatAlertMessage(timerAlertTemplate, toolbox, countdown.label), { persistent: true });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ export function useToolboxAlerts({ toolboxes, moduleData, links, route, drawerGa
|
|||
if (alertMs) nextTargets.set(alertKey, alertMs);
|
||||
|
||||
if (alertMs && previousAlertMs > lastTickMs && previousAlertMs <= nowMs) {
|
||||
notify(formatAlertMessage(calendarAlertTemplate, toolbox, event.title));
|
||||
notify(formatAlertMessage(calendarAlertTemplate, toolbox, event.title), { persistent: true });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ export function useToolboxAlerts({ toolboxes, moduleData, links, route, drawerGa
|
|||
const resumeKey = `${key}:${currentOccurrence.startMs}`;
|
||||
if (resumedCalendarEventsRef.current.has(resumeKey)) return;
|
||||
resumedCalendarEventsRef.current.add(resumeKey);
|
||||
notify(formatAlertMessage(calendarResumeTemplate, toolbox, event.title));
|
||||
notify(formatAlertMessage(calendarResumeTemplate, toolbox, event.title), { persistent: true });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,14 +32,12 @@ if (assetRecoveryUrl.searchParams.has("_asset-recovery")) {
|
|||
}
|
||||
|
||||
const ToolboxDrawer = lazy(() => import("./features/toolboxes/ToolboxPages.jsx").then((module) => ({ default: module.ToolboxDrawer })));
|
||||
const DEFAULT_NOTIFICATION_DURATION_MS = 120000;
|
||||
const SHORT_NOTIFICATION_DURATION_MS = 5000;
|
||||
const DEFAULT_NOTIFICATION_DURATION_MS = 6000;
|
||||
|
||||
function getNotificationDuration(message, options = {}) {
|
||||
function getNotificationDuration(_message, options = {}) {
|
||||
if (options.persistent) return 0;
|
||||
if (Number.isFinite(options.durationMs)) return Math.max(1000, Number(options.durationMs));
|
||||
const normalized = String(message || "").toLowerCase();
|
||||
return /\bcopi[ée]|\bcoll[ée]|presse-papiers/.test(normalized) ? SHORT_NOTIFICATION_DURATION_MS : DEFAULT_NOTIFICATION_DURATION_MS;
|
||||
return DEFAULT_NOTIFICATION_DURATION_MS;
|
||||
}
|
||||
|
||||
function App() {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { HomeOnboardingSection } from "./HomeOnboardingSection.jsx";
|
|||
export function HomePage({ siteContent, toolboxes, actions }) {
|
||||
const content = siteContent.home;
|
||||
const toolCount = Object.keys(TOOLBOX_MODULES).length;
|
||||
const hasToolboxes = toolboxes.length > 0;
|
||||
const showOnboarding = import.meta.env.DEV || toolboxes.length === 0;
|
||||
return (
|
||||
<>
|
||||
|
|
@ -15,7 +16,11 @@ export function HomePage({ siteContent, toolboxes, actions }) {
|
|||
<h1>{content.hero.title}</h1>
|
||||
<p>{content.hero.description}</p>
|
||||
<div className="actions nds-toolbar">
|
||||
{hasToolboxes ? (
|
||||
<a className="nds-button" data-size="md" data-variant="primary" href="#/toolboxes">{content.hero.primaryAction}</a>
|
||||
) : (
|
||||
<button className="nds-button" data-size="md" data-variant="primary" type="button" onClick={() => actions.setCreateModal({ tutorial: true })}>{content.hero.createAction}</button>
|
||||
)}
|
||||
<a className="nds-button" data-size="md" href="#/games">{content.hero.secondaryAction}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -427,14 +427,6 @@
|
|||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.tool-add-modal {
|
||||
width: min(420px, calc(100vw - 32px));
|
||||
}
|
||||
|
||||
.tool-add-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
|
|
|
|||
|
|
@ -13,12 +13,19 @@
|
|||
font-weight: 800;
|
||||
}
|
||||
|
||||
.results-copy-button {
|
||||
.results-copy-button.nds-icon-button {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.results-copy-button.nds-button {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
min-height: 32px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.results-checklist-modal.nds-modal {
|
||||
width: min(560px, calc(100vw - 32px));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
// Rôle : stylise les modales de création et d'ajout d'outil.
|
||||
.tool-add-modal {
|
||||
width: min(560px, calc(100vw - 32px));
|
||||
@use "../design-system/controls";
|
||||
|
||||
.tool-add-modal.nds-modal {
|
||||
width: min(900px, calc(100vw - 32px));
|
||||
scrollbar-gutter: stable;
|
||||
@include controls.scrollbar-control;
|
||||
}
|
||||
|
||||
.tool-add-modal header {
|
||||
|
|
@ -21,30 +25,67 @@
|
|||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.tool-add-category-list,
|
||||
.tool-add-category-section {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.tool-add-category-section h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.tool-add-category-section h3::after {
|
||||
height: 1px;
|
||||
flex: 1;
|
||||
background: var(--color-border);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.tool-add-card {
|
||||
display: grid;
|
||||
height: 98px;
|
||||
min-height: 96px;
|
||||
min-width: 0;
|
||||
grid-template-columns: 28px minmax(0, 1fr);
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
gap: 8px;
|
||||
padding: var(--space-3);
|
||||
align-items: center;
|
||||
gap: 4px 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(21, 26, 48, 0.72);
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tool-add-card span:last-child {
|
||||
.tool-add-card strong {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tool-add-card > span:last-child {
|
||||
width: 100%;
|
||||
grid-column: 1 / -1;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.tool-add-card .module-icon {
|
||||
flex: 0 0 34px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.tool-add-card .module-icon-svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.tool-add-card:hover {
|
||||
|
|
@ -53,6 +94,18 @@
|
|||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.tool-add-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.tool-add-grid {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-create-modal form,
|
||||
.toolbox-link-modal form {
|
||||
display: grid;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue