Sur cette page

← Toute la documentation

Menus : comportement et thèmes

Menus : comportement, API de comportement et thèmes, sélecteur de modificateurs.

Les menus séparent ce qu'ils font de ce à quoi ils ressemblent :

  • une couche de comportement typée (apps/web/src/menu/) : des stores nommés (ce que les menus affichent, en valeurs JSON) et des actions nommées (ce qu'ils peuvent faire), sans balisage ;
  • les composants Svelte de l'interface, écrits sur cette couche (ils lisent les mêmes stores et appellent les mêmes actions) ;
  • les thèmes d'interface (Classique, Slant, Cabinet…) : des données (un manifeste, des jetons CSS validés par l'hôte, des images) qui restylent ces composants sans les remplacer, voir thèmes et interfaces. Il n'y a plus de modèle de menus en HTML : un thème tiers ne contient ni balisage ni script.
flowchart LR
  host[Hôte Rust<br/>window.ipc] <--> shell[Page principale<br/>bridge.ts, host.ts]
  shell --> behavior[Couche de comportement<br/>stores + actions]
  behavior --> svelte[Composants Svelte]
  themes[Thèmes<br/>jetons validés] --> svelte

Cette couche couvre la sélection des maps ; Réglages, Mods, Skins et l'Atelier s'ouvrent par ses actions, comme le menu Échap.

Sélection des maps

La liste, à droite, montre les beatmapsets (dossiers de morceaux). Comme dans osu!, un clic sur un set l'ouvre sur place : ses difficultés s'affichent dessous, chacune avec sa disposition (« 4K »), son nom et la note de la vue enregistrée par le mod choisi. Une valeur absente reste « — » et prend une couleur neutre ; une valeur MinaCalc utilise une rampe adaptée au MSD, une note en étoiles utilise le spectre des étoiles. Ouvrir un set sélectionne sa difficulté déjà choisie, sinon la première ; un clic sur une difficulté la sélectionne ; un clic sur le set ouvert le referme. Un seul set est ouvert à la fois. Le panneau de gauche suit la sélection : titre, artiste, note choisie et statistiques essentielles, puis les panneaux d'analyse déclarés par le mod de rating. Le thème ne décide pas de leur contenu à partir du nom du calculateur : Etterna choisit un radar accompagné de barres portant les vrais noms des skillsets, osu un profil du chart et une timeline. Etterna déclare une échelle fixe de 0 à 40 : 10 remplit un quart de barre, 20 la moitié ; au-delà de 40, le remplissage sature sans changer le nombre affiché. Une valeur absente n'a pas de remplissage.

Les onglets Infos / Classement / Mods sont directement sous les statistiques, avant leur contenu ; Local / Online reste un sous-choix compact du classement. Seule la liste des scores défile : le titre, les statistiques, les onglets, Local / Online, le sélecteur Performance et le pied de page restent fixes. Le classement n'affiche pas de paragraphes explicatifs sur le jugement courant, la performance sélectionnée ou les valeurs du simulateur.

Mods contient le rate natif (playbackRate), de 0,50 à 2,00 par pas de 0,05, avec saisie, curseur et retour à 1, sous le simple libellé « Rate ». Aucun effet inactif ni paragraphe explicatif sur le rate ou les données de développement n'est proposé. Changer le rate garde l'analyse cohérente précédente pendant le calcul, sans message de chargement qui clignote. Les erreurs restent signalées dans un emplacement fixe.

Sous le rate, Mods liste un interrupteur compact par modificateur de jeu publié par les mods (gameplayMods), par exemple Auto de pvng.auto : libellé seul, description dans une info-bulle personnalisée au survol ou au focus, rien d'autre. Le choix est le réglage gameplayMods, retiré avec une notification si le mod qui fournit le modificateur disparaît. Les nombres rejoignent leur nouvelle valeur sans recréer les graphes ni déplacer les contrôles. Une interruption repart du nombre affiché ; le mouvement réduit désactive cette interpolation, exclusivement visuelle. Les réponses d'un ancien chart, rate ou calcul sont ignorées. Les cartes de score et la page du replay affichent le rate enregistré, sans signe de multiplication, avec leur propre densité et difficulté recalculées nativement. Changer le rate du menu ne réécrit pas ces parties. La page du replay a un sélecteur Difficulté à côté de Jugement et de Performance : c'est le même composant (RatingPicker) et le même réglage (ratingSystem) que le sélecteur du pied de la sélection, donc UN seul choix pour tout le jeu (changer l'un change l'autre, comme pour Jugement et Performance). Mêmes calculateurs, groupés par mod, recherche et clavier ; masquer une difficulté reste une action séparée dans les Réglages. La valeur, colorée par ratingColor et arrondie à 2 décimales, est celle du chart au rate enregistré du replay (analyse native de ce rate, jamais une valeur 1x multipliée) ; le rate est écrit dessous (« à la vitesse 1.50 »). Changer de calculateur lit simplement une autre valeur déjà calculée : le nombre rejoint sa nouvelle valeur sur place, sans rechargement. Un calculateur sans valeur pour ce chart affiche « Indisponible » dans le même gabarit (le nom prend toujours deux lignes), sans rien déplacer. Les cartes de classement n'affichent pas la difficulté : leur ligne porte déjà rang, joueur, rate, combo, précision, performance et paliers, une valeur de plus la surchargerait ; la page du replay l'affiche. Le pied du panneau reste visible sur une seule ligne : Note et Jugement, cliquables sur toute leur surface, puis Jouer à droite. Les menus sont dessinés par le thème (pas des select natifs), regroupés par mod, à hauteur bornée, avec recherche au-delà de huit choix. ↑/↓, Début/Fin et Entrée parcourent et choisissent ; Échap ferme sans ouvrir la navigation. Les paramètres du jugement choisi restent dans son menu. Réglages distingue Utiliser de Masquer/Afficher une note, sans case à cocher.

Les Réglages s'ouvrent dans une grande fenêtre modale centrée, au-dessus de la sélection assombrie. La bibliothèque reste montée : recherche, set ouvert, difficulté choisie et position de défilement sont conservés. Les catégories (une icône chacune) sont à gauche, la recherche en haut ; seul le contenu défile (structure, recherche, restauration, export/import : reglages.md). Les changements valides s'appliquent immédiatement, sans bouton Appliquer. Le bouton de fermeture ou Échap rend directement la main à la sélection, sans ouvrir la navigation. Une liste déroulante ou une capture de touche active consomme d'abord Échap. Le focus reste dans la fenêtre et revient à son point d'entrée disponible ; les notifications de réglages sont affichées dans la fenêtre pour rester lisibles et refermables.

La catégorie Scroll speed est indépendante de Jeu et de Jugement. Elle présente les fournisseurs des mods, dont Prism en ms, et accepte les décimales libres hors du curseur suggéré. Le bouton Jouer suit scrollSpeed.playDisabled, pour ne pas jouer une conversion encore inconnue.

Réglages → Jugement affiche simplement le nom du jugement actif (« Etterna · J4 », par exemple), avec un petit chevron : cliquer sur ce nom ouvre la liste, sans gros champ pleine largeur ni grille permanente. Un choix, un second clic sur le nom, un clic extérieur ou Échap ferme la liste ; elle s'ouvre au-dessus ou au-dessous selon la place disponible. Les paramètres ont un curseur dessiné par le thème et une boîte de valeur éditable avec boutons −/+. Entrée valide, Échap annule une saisie non validée ; les bornes, pas et combinaisons disponibles du mod restent respectés. Sous les curseurs, les fenêtres de chaque palier sont affichées en millisecondes avec leurs couleurs et se mettent à jour immédiatement depuis les tables de l'hôte. « − » signifie avant la note, « + » après ; « ± » indique la même tolérance des deux côtés. Le petit menu de la sélection affiche aussi ces valeurs sous ses curseurs ; les Réglages conservent le tableau détaillé.

Le thème distingue titre, note principale et informations secondaires ; les lignes de maps ont des accents liés à leur difficulté plutôt qu'une bordure identique partout. Sélection, onglets et menus ont de courtes transitions, désactivées avec prefers-reduced-motion: reduce. Les timelines colorent les valeurs basses en bleu/cyan et les valeurs hautes en tons chauds. Le curseur donne la valeur et le temps exacts du bin, à la souris comme au clavier ; la couleur ne remplace pas les nombres. Les timelines, les points radar et leurs lignes de valeur utilisent la même infobulle personnalisée : libellé complet déclaré par le mod, valeur réelle, unité et intervalle temporel ou échelle déclarée. Une donnée manquante reste « Indisponible ». Le curseur temporel et les lignes du radar sont accessibles au clavier ; l'infobulle accepte le pointeur et reste bornée à la fenêtre. Échap ou un clic extérieur la ferme sans ouvrir le menu de navigation. Le graphe utilise l'espace disponible (160 à 240 px de haut) au lieu de laisser un grand vide avant les contrôles. À faible hauteur, les panneaux conservent leurs tailles lisibles et le défilement reste dans les détails, jamais sur toute la page.

Classement propose Local / En ligne. Local affiche un seul classement de cartes cliquables : joueur, rang, précision, combo et performance au-dessus de la date. Les petits compteurs colorés donnent les hits de chaque palier du jugement courant ; Miss n'est pas répété comme une deuxième métrique. Cliquer sur une carte ouvre la page dédiée au replay. Toutes les entrées sont rejugées depuis les appuis et relâchements du replay avec le jugement actuellement choisi, pas avec celui de la partie d'origine. Changer de jeu de jugement ou de paramètres relance l'évaluation hors du thread d'interface ; les anciens résultats ne remplacent jamais la demande courante. Les replays absents ou illisibles sont signalés, sans utiliser leur ancienne précision comme résultat de remplacement. L'historique complet est consultable par pages, sans plafond de douze ou vingt scores. L'hôte garde le classement réévalué en cache ; demander la suite ne rejuge pas tous les replays. Les pages d'une ancienne map ou d'une ancienne évaluation sont refusées.

Le nom est enregistré dans chaque nouveau replay. Réglages → Interface permet de choisir le nom pour les prochaines parties ; sans saisie, l'application native utilise le nom du compte Windows. Un ancien replay sans nom reste signalé comme tel, sans être attribué rétroactivement au joueur actuel.

Sur bureau, la page du replay place le joueur, la précision dominante et les compteurs à gauche, l'analyse à droite, sans grille de tuiles arrondies. Le chart reste en en-tête. La liste verticale nom / nombre ne suppose jamais six paliers : les 32 paliers personnalisés et Miss restent accessibles dans son propre défilement, sans déplacer le score ou les graphes. Le remplissage de chaque ligne représente sa part réelle dans le total des jugements. Les onglets Timing, Distribution et Progression évitent d'empiler ou de miniaturiser tous les graphes ; ←/→, Début et Fin permettent aussi de les choisir au clavier. La typographie reste lisible et la page tient presque sans défilement aux tailles de bureau usuelles. Les axes sont en HTML, sans déformation par le SVG. Le timing montre un point par événement du juge natif, à son instant exact, avec sa touche, son palier et son offset réel, sans moyenne ni barre min–max. Le Canvas dessine tous les événements, y compris les frappes simultanées, sans échantillonnage. Survoler ou focaliser une touche la met en évidence ; cliquer la conserve, et « All keys » rétablit toutes les colonnes. Les flèches, Début et Fin parcourent aussi les touches au clavier. Une colonne sans frappe mesurée garde ses Miss et des statistiques indisponibles, jamais un faux zéro. Chaque Miss est une croix rouge vif sur la ligne centrale, à son instant de jugement, sans offset. Les croix passent au-dessus des points pour ne pas être masquées par une frappe parfaite. Elles n'entrent ni dans les statistiques d'offset ni dans l'histogramme. Les fenêtres avant/après du jugement actif gardent leur fond discret et leurs limites colorées, même asymétriques. L'aide près de « Hit offset » explique les offsets signés et les croix Miss. Le survol inspecte l'événement le plus proche ; le contrôle sous le graphe parcourt les événements par temps, avec leur identité pour distinguer les notes simultanées. L'infobulle donne la touche, le palier, l'instant exact et l'offset réel, ou « Miss · no offset ». ←/→, Début et Fin parcourent les notes ; Échap ferme l'infobulle sans quitter le replay. Le bouton LN ouvre une analyse compacte : têtes manquées, relâchements anticipés, relâchements dans la fenêtre et notes tenues jusqu'au bout. Les moyennes et étendues des têtes et relâchements viennent des événements du moteur natif, avec le modèle de tenue du jugement actif. Une tête réellement pressée reste mesurable même si la tenue échoue ensuite ; une fin automatique ne fabrique pas de relâchement à zéro ni de jugement de queue indépendant. La distribution cadre initialement les bins renseignés (sans perdre de hits) et garde le zéro en référence. Ses barres reprennent les couleurs des fenêtres du jugement courant, avec leurs bornes avance/retard indépendantes. Un bin traversant une borne change de couleur à cet endroit, sans prétendre connaître la répartition exacte de ses frappes entre les paliers.

Les trois vues proposent un zoom à la molette, ancré sous le pointeur, ou avec les boutons −/+, jusqu'à 32 fois : la molette seule zoome l'axe horizontal (le temps, ou l'offset pour la distribution), Maj + molette zoome l'axe vertical (offset ±ms du Timing, effectif de la distribution, échelle de chaque courbe de la Progression, qui partagent le même zoom vertical ; les bornes visibles s'affichent sous chaque courbe). Le groupe de boutons ↕−/↕+ en est l'équivalent. Une fois zoomée, la vue se déplace par glisser (les deux axes à la fois) ou avec les flèches ; Full view retrouve l'étendue complète des deux axes. Au clavier, le graphe sélectionné répond à + / − (axe horizontal), Maj + ↑ / ↓ (axe vertical), aux flèches (déplacement) et à 0 (vue complète). Timing et Progression partagent la même fenêtre temporelle ; la distribution garde sa fenêtre d'offset indépendante. Les axes et l'inspection suivent l'étendue visible, sans agrandir les textes ni inventer de nouveaux points. Le clavier de Timing et le curseur d'intervalle de Progression ramènent la mesure choisie dans la vue. Le zoom (horizontal et vertical) est conservé au changement de jugement ou de calculateur, et borné si l'étendue des données change. L'état des vues est dans lib/graph-view.ts (fonctions pures, testées).

Le décalage moyen (panneau de gauche, onglet Timing, intervalle de la Progression, invite de décalage audio, détail d'une case de la carte d'erreurs) est coloré selon son éloignement de zéro, en dégradé continu : blanc jusqu'à 2 ms, jaune vers 4 ms (« pas terrible, fais attention »), orange vers 7 ms (« change tes réglages »), rouge à partir de 10 ms (« change tes réglages maintenant »). Les quatre couleurs sont les jetons --pv-offset-ok/-warn/-high/-bad (un thème clair les redéfinit) ; lib/offset-color.ts (offsetColor, ancres nommées) calcule le mélange. Le signe (avance/retard) reste dans le texte, et le conseil est une infobulle et un texte lu par les lecteurs d'écran.

Le combo est bleu, la précision violette en pointillés ; la densité conserve un remplissage discret et une couleur ambre/orange/rouge liée à son amplitude. Les transitions de vue ne déplacent pas les axes. La progression conserve les valeurs de combo et précision de fin d'intervalle, sans interpolation. Combo, précision et densité du chart (notes/s) sont superposés dans un seul graphe. Les trois boutons de légende affichent ou masquent leurs courbes indépendamment, à la souris ou au clavier. Les échelles et unités sont indiquées dans ces contrôles, sans colonnes de valeurs ni espace réservé à gauche du graphe. L'axe temporel et le curseur restent communs. Masquer une courbe ne redimensionne pas les autres échelles et ne déplace pas le graphe ; tout masquer affiche une invitation à en choisir une. Les aplats des intervalles contenant des Miss sont masqués par défaut. Le bouton Miss intervals affiche ou masque ceux du replay et de sa référence, indépendamment des courbes, sans changer les axes ni le curseur. Leur nombre reste affiché à l'inspection, sans position exacte inventée. Les bins de densité gardent leur largeur native, indépendamment des intervalles du replay ; la valeur inspectée correspond à la fin de l'intervalle sélectionné. Une densité absente désactive son bouton, sans afficher de courbe à zéro. Les statistiques proviennent des événements du juge natif ; l'absence d'offset d'un Miss n'est jamais prise pour une frappe parfaite. Le timing complet est chargé par pages de 2048 événements depuis le même snapshot natif, sans relire ni rejuger le replay à chaque page. Une nouvelle analyse et ses historiques de points sont publiés ensemble, une fois toutes les pages reçues ; les pages obsolètes ne peuvent pas remplacer la paire affichée. Seules Progression et Distribution conservent leurs agrégats bornés. Le retour au classement conserve la map, les pages de scores chargées et la position dans la bibliothèque.

Compare score choisit un autre score du même chart, avec accès à toutes les pages du classement et une action pour retirer la référence. Une seule requête évalue les deux replays sous le même jugement et calculateur courants ; une référence supprimée ou illisible ne fait pas disparaître le score principal. La référence affiche son joueur, son rate enregistré et les écarts de précision, combo, difficulté et performance disponibles. Ses points de timing sont creux, aux couleurs des paliers, et ses croix Miss rouges sont entourées d'un cercle. Ses courbes de progression sont en pointillés et sa distribution tracée en contour. Le bouton Reference masque ces tracés sans changer les échelles. Si les rates diffèrent, les temps de la référence sont alignés par position du chart (temps × rate référence / rate principal) ; l'axe garde les secondes réelles du replay principal, et les offsets restent en millisecondes réelles.

Watch replay lance la lecture native des entrées physiques enregistrées, avec le rate du replay et le jugement actuellement affiché. Le playfield, l'audio et le HUD suivent cette lecture ; l'overlay indique REPLAY et son rate. Les appuis du spectateur ne modifient pas les notes ; Échap revient à l'analyse conservée, comme la fin naturelle. La lecture ne crée ni score ni nouveau replay. Dans le serveur de développement uniquement, un simulateur clairement identifié visualise le chart et les entrées fictives, sans prétendre être le rendu ou le juge natif.

Changer le jugement ou le calculateur ne démonte plus l'analyse : le dernier résultat cohérent reste affiché pendant le recalcul. Les nombres montent ou descendent vers les nouvelles valeurs en 260 ms ; une interruption repart du nombre affiché. Cette interpolation ne modifie ni les stores, ni les calculs, ni les résultats enregistrés. Les graphes conservent une transition de 180 ms, sans inventer de points entre deux agrégats. L'onglet, le curseur, les courbes masquées et le défilement sont conservés. La performance et son unité appartiennent au même résultat, sans mélange entre ancienne précision et nouveau calculateur. Les réponses périmées restent ignorées. L'animation est désactivée, ou arrêtée si elle est en cours, avec la préférence de mouvement réduit. Le simulateur génère sa densité depuis les mêmes notes que ses replays ; il n'étire pas un graphe indépendant pour masquer une durée différente.

Références visuelles : résultats Etterna, interface Quaver et résultats osu!mania.

Par défaut, la performance suit le jugement actif : précision osu!mania → pp osu! (calculateur Metron osu-2018, affiché « osu!mania », en pp), Wife3 → SSR MinaCalc 515. Le joueur peut choisir un autre calculateur avec le sélecteur Performance visible au-dessus du classement, sur la page du replay ou dans les Réglages. Il n'est pas caché dans le menu du jugement. La note de difficulté reste séparée. La précision du jugement courant est passée au calculateur choisi : une combinaison de modèles différents est indiquée comme non standard, et non comme des pp/SSR officiels. Seuls osu-2018 (pp) et etterna-515 (SSR) ont une performance ; les entrées qu'un calculateur exigerait en plus ne sont jamais inventées.

L'index SQLite est écrit après la sauvegarde du replay ; les parties interrompues sont exclues. L'identité comprend le chemin du chart et son indice de difficulté. Les anciens replays sans cette identité ne sont pas rapprochés par titre. En ligne indique l'absence de service, sans inventer de scores.

Dans l'hôte web de développement (bun run dev), chaque difficulté de la bibliothèque fictive reçoit un historique déterministe fourni, avec des noms de joueurs de démonstration et des entrées simulées rejugées lorsque le jugement change. Les mêmes données réapparaissent après rechargement ; la démo reste vide. Les parties terminées du simulateur s'ajoutent à cet historique en mémoire, propre à chaque chart. MockOptions.replays permet de fournir des entrées de replay ; scores fournit des lignes déjà évaluées pour les scénarios d'interface uniquement. scores: {} laisse tous les classements vides jusqu'à une partie terminée. Le mock affiche des valeurs numériques déterministes de pp/SSR pour travailler la présentation, comme demandé : ce sont des fixtures de développement, pas une réimplémentation de Metron. Ses graphes restent issus des entrées simulées. Ces fixtures ne sont jamais écrites dans la bibliothèque native et ne rendent pas le service en ligne disponible.

Touche Action Effet
↓ library.down difficulté suivante du set ouvert, sinon première difficulté du set suivant
↑ library.up difficulté précédente, sinon dernière difficulté du set précédent
→ / ← library.nextSet / library.previousSet set suivant / précédent, sur sa première difficulté
Entrée play joue la difficulté sélectionnée
Échap navigation.drawer ferme d'abord les Réglages ouverts ; sinon ouvre ou ferme le menu

Sans sélection, ↓ ouvre le premier set visible. Les touches de la sélection ne jouent que sur la page Bibliothèque, menu et Réglages fermés. Dans un champ de texte (la recherche), seules Échap, Entrée, ↑ et ↓ passent ; sur un bouton hors de la liste, Entrée et Espace restent au bouton ; sur un curseur, une liste déroulante, dans une boîte de dialogue ou une popover, seule Échap passe. La molette et la barre de défilement ne font défiler que la liste.

Bande de difficultés et changements de map

Chaque carte de set porte une bande de difficultés : autant de pastilles que la largeur de la bande en laisse sur une seule ligne, de la plus facile à la plus dure sur la note choisie, chacune colorée par ratingColor, puis un badge « +N » (N = difficultés non montrées, libellé accessible « plus N difficultés ») dont le survol ou le focus clavier de la carte ouvre le popover partagé (ChartPopover) listant toutes les difficultés. Selon la place, une pastille est un simple trait coloré, la note arrondie, ou la disposition et la note (menu/difficulty-strip.ts, fitStrip) : le calcul ne dépend que de la largeur de la liste (mesurée une fois, jamais par ligne ni par pastille) et du nombre de difficultés, car les largeurs de pastille sont fixes. La hauteur est constante, rien ne déborde et aucune ligne ne bouge : la liste reste virtualisée.

Choisir une autre map met à jour le panneau de gauche en place : le titre et les graphes se fondent, les nombres défilent (AnimatedNumber), et le panneau garde la dernière analyse livrée jusqu'à l'arrivée de la nouvelle, sans vider ni saut de mise en page. L'animation d'entrée des panneaux ne se rejoue qu'une fois la sélection posée : menu/settled.ts (createEntrance) pose data-entering sur .selected-map 250 ms après le dernier changement (le délai repart à chaque changement), seulement si la map posée appartient à un autre set que la précédente (changer de difficulté dans un set ne rejoue rien), jamais en mouvement réduit. Le déclencheur est commun aux trois interfaces ; chaque interface décide dans son CSS de ce que .selected-map[data-entering] anime.

Liste virtuelle

LibraryBrowser (apps/web/src/menu/library-browser.ts) garde la liste indépendante de son rendu :

  • Géométrie. Chaque set occupe setHeight px (pas de ligne, espacement compris) ; le set ouvert ajoute nombre de difficultés × difficultyHeight. Avec un seul set ouvert, la position d'un set et le set sous un décalage se calculent directement (ListLayout), sans mesurer le DOM. Le rendu déclare ses pas (library.metrics) ; changer de rendu garde le premier set visible en haut.
  • Hauteur stable. Dès la première page, la hauteur totale vaut total × setHeight ; l'arrivée des pages ne la change jamais. Ouvrir un set réserve la hauteur de ses difficultés, connue puisque sa page est chargée ; si un set ouvert plus haut se referme, le défilement est corrigé pour que le set cliqué reste sous le pointeur.
  • Pages par index de set. Pages de 80 sets (libraryPage, décalage multiple de 80), 12 gardées au plus (les plus éloignées de la vue partent) ; sont demandées les pages qui recouvrent la vue plus 5 sets de chaque côté, et la suivante. Un saut de la barre de défilement demande directement la page visée ; en attendant, des lignes vides de même hauteur la remplacent.
  • Lignes bornées. Seuls les sets autour de la vue et, pour le set ouvert, les difficultés proches de la vue sont rendus.
  • Clavier. Un déplacement vers un set dont la page n'est pas chargée la demande et l'ouvre à son arrivée ; toute autre action annule cette attente. Le déplacement fait défiler juste assez pour montrer la difficulté choisie.
  • Réponses périmées. Une page d'une ancienne recherche ou d'une bibliothèque changée (requestId différent) est ignorée ; la sélection (lib/selection.ts) ignore les réponses d'une sélection remplacée et reste interactive pendant le chargement. Les notes arrivent avec la page ou la sélection ; le rattrapage du mod rafraîchit la note et les colonnes de détail de chaque vue, sans relancer la sélection ni perdre le résultat d'une partie.
  • Recherche structurée. libraryPage.query est un LibraryQuery généré par @pvng/db : { text, filters: [{ key, value }] }. Le texte libre attend 150 ms après la dernière frappe ; ajouter, modifier ou retirer un filtre applique immédiatement le texte courant. Tous les mots et tous les critères doivent correspondre à la même difficulté. Les sets ne contiennent que les difficultés correspondantes, avec leurs indices natifs inchangés ; chercher ne resélectionne jamais un chart. La recherche reçoit le focus à l'ouverture ; + Filtre ouvre les champs regroupés par fournisseur, puis un éditeur compact.
  • Catalogue extensible. libraryFilters publie le catalogue complet et sa révision monotone. Le sélecteur groupe les champs neutres dans « Chart » et les contributions par le vrai nom de leur mod. Le thème par défaut propose une liste recherchable au clavier, puis un éditeur de bornes numériques inclusives ou de texte « contient / égal à ». Les chips permettent de modifier et retirer chaque critère. Échap ferme le sélecteur ou annule l’édition avant d’ouvrir le menu de navigation ; un clic extérieur referme le sélecteur.
  • Validation et disponibilité. Au plus 512 caractères de texte libre et 16 critères ; au moins une borne numérique finie (négatifs et décimaux autorisés, minimum ≤ maximum), ou 1 à 256 caractères de texte non blanc. La recherche textuelle est littérale, sans distinction de casse ASCII : %, _ et \ ne sont pas des jokers. Un fournisseur supprimé, désactivé, en échec, ou dont le champ change de type laisse un chip indisponible : les résultats sont bloqués jusqu’au retour du fournisseur ou au retrait explicite du critère. Les anciennes révisions du catalogue sont ignorées.
  • Calculs en arrière-plan. Sans filtre de mod, les notes sont corrigées en place : ni reset, ni changement de défilement, de set ouvert ou de sélection. Avec un filtre de mod, le rattrapage peut changer les résultats : le navigateur demande au natif le rang filtré de deux identités au plus (libraryPage.anchors : dossier de la vue, dossier ouvert). La réponse library.anchors associe chaque dossier à son rang exact, ou null s’il ne correspond plus. Le rang suit le premier indice original du dossier, pas celui de la première difficulté restante. Le navigateur charge directement les pages de ces rangs et de la vue, puis les remplace atomiquement : même dossier ouvert et même décalage dans la vue, même après un déplacement de milliers de sets, sans charger toute la bibliothèque. Les anciennes lignes restent utilisables pendant cette préparation. La sélection native n’est jamais remplacée ; si les rangs ou le total changent pendant les requêtes bornées, les ancres sont résolues à nouveau.

Les composants n'implémentent aucun calcul de difficulté et ne reçoivent pas la bibliothèque entière. Le mock Web applique les mêmes conjonctions et la pagination côté hôte, avec des notes et tags déterministes ; désactiver/recharger leurs mods exerce le cycle de vie réel du catalogue.

API de comportement

apps/web/src/menu/behavior.ts exporte stores, actions, invoke(nom, arguments) et act(nom, ...arguments) (version typée pour les composants). Les types des valeurs viennent des types générés @pvng/* : les vues (views.ts) sont calculées à partir de BeatmapSet, LibraryEntry, SongInfo, Analysis, JudgementSetInfo… ; aucun type d'échange avec Rust n'est réécrit. StoreValue<nom> et ActionArgs<nom> donnent le type d'un store et des arguments d'une action.

Stores

Store Contenu
library query (texte saisi), filtered (texte ou critères actifs), total, loaded, empty, folders, hasFolders, scanning, progress, percent, issues (nombre), locked (hôte absent ou import en cours)
libraryFilters ready, revision, options (clé, label traduit, groupe, type, unité), groups (name, options), active (métadonnées, summary, unavailable, removeLabel), editor (key, label, group, unit, number, available, min, max, text, match), error traduit, blocked, status traduit, limit (16 critères atteints)
scrollSpeed ready, scrollMs (temps exact résolu ou null), playDisabled (verrou de bibliothèque ou conversion indisponible)
sets la liste virtuelle : extent (px), rows, total, loaded, expanded (position, -1 sans set ouvert), scroll ({ top, seq } : demande de défilement)
selection hasSong, loading, demo, title, artist, creator, difficulty, keys, keysText, color, rating, ratingUnit, duration, bpm, notes, holds, averageNps, peakNps, panels, density, binSeconds, background ; value, durationSeconds, bpmMin, bpmMax sont les nombres résolus pour une présentation animée
chartPanel info, leaderboard ou mods : vue du chart sélectionné
visualizer open (le panneau de la liste est remplacé par l'aperçu du chart), status (loading, playing, paused, ended, closed), positionUs (dernier rapport de l'hôte), durationUs, playbackRate, seeking (docs/visualiseur.md)
playbackRate value, min, max, step, loading, error, available, displayedRate, mock ; displayedRate identifie le dernier résultat cohérent pendant le recalcul
gameplayMods label, mods : modificateurs de jeu publiés (key, modId, kind, name, short, description, enabled, group, icon, conflicts, params) pour le sélecteur de l'onglet Mods
leaderboardSource local ou online
leaderboard loading, loadingMore, entries, total, evaluationId, unavailableReplays, performance, performanceLabel, mock : classement paginé rejugé avec le jugement courant, noms enregistrés, compteurs par palier et performance choisie (pp / SSR, ou null)
performance chosen (chaîne vide pour Auto), label, choices : calculateurs natifs de performance disponibles, indépendants de la note de difficulté
replay open, loading, replayId, evaluationId, detail, judgement, error, performance, performanceLabel, mock, analysis, rating, ratingUnit, ratingLabel, color : analyse au rate enregistré, avec jugement et calculateur courants
judgement ready, label (« osu!mania · OD 8 »), sets (key, name, mod, active), tiers (index, name, color, gradient?), refusal (message localisé quand aucun mod de jugement n'est installé et que le choix est un jeu de mod, sinon null : play ne fait alors rien)
rating chosen (<mod>/<id>), calculator, label, choices (notes affichables), options (toutes, avec hidden et performance) ; le calculateur détermine les nombres de sets et selection, jamais les règles de jugement
navigation page (page sous-jacente), drawer, settings (fenêtre modale ouverte), pages (id, label, active)
result visible, aborted, auto (partie Auto : ni score ni replay), accuracy, maxCombo, tiers (name, color, gradient?, count) ; résumé masqué après dix secondes, sans toucher aux scores enregistrés ni à l'analyse de replay
i18n locale, messages (catalogue de la langue, anglais en repli)
modOptions groups (modId, name, elements : key, name, resettable, options : name, label, kind, min, max, step, values, maxLength, value, default, resettable) : les options que les mods déclarent player: true (leurs réglages propres, globaux), générées depuis leurs déclarations
skinCustomization editing (session de l'éditeur de skin en cours) et skins : un élément par skin installé, avec id, selected, customized (la configuration du joueur n'est pas vide), playfield (valeurs du playfield réglées), lanes (colonnes qui ont leurs propres valeurs), elements (widgets du HUD réglés), imported (images importées, null tant que l'hôte ne les a pas signalées) et canCustomize (le skin en cours, hors session)

selection.panels fournit les panneaux résolus dans l'ordre du mod. Chaque panneau a kind et title ; les panneaux de valeurs ont fields (label, unit, decimals, value ou null, display prêt à afficher), les timelines ont series (label, source, unit, values, binSeconds).

Une ligne de sets.rows a kind, key, position, top, height et :

  • set : expanded, selected, set (id, title, artist, creator, count, countText, keysText, ratingText, color, chips, thumbnail) ; chips liste ses difficultés de la plus facile à la plus dure sur la note choisie (index, name, keysText, value, rating, short = note arrondie, color = ratingColor), les non notées en dernier ;
  • difficulty : difficulty (rang dans le set), selected, last, entry (index, name, keys, keysText, rating, ratingUnit, color, notes, duration) ;
  • placeholder : une page pas encore arrivée.

Les textes affichés (« 3 difficulties », « 4K », « 12.40–21.03 MSD ») sont déjà traduits : tous les thèmes montrent les mêmes valeurs.

Actions

Action Arguments Effet
play — joue la sélection (sans effet pendant un import)
chart.panel info, leaderboard ou mods change le panneau du chart, sans le resélectionner
editor.open aucun ouvre la map sélectionnée dans l'éditeur de maps (bouton « Éditer » à droite des onglets) et affiche la page de l'éditeur ; une map qui n'est pas un .rox est éditée par une copie .rox à côté, l'original n'est jamais réécrit
gameplayMod.set clé <modId>/<id>, booléen active ou désactive un modificateur publié (sans effet pour une clé inconnue) ; synchronise Settings.gameplayMods
playbackRate.set nombre règle le rate natif, borné entre 0,5 et 2 par pas de 0,05 ; synchronise Settings.playbackRate
leaderboard.source local ou online change la source affichée du classement
leaderboard.more — charge la page suivante du classement courant
replay.open identifiant d'un replay du classement ouvre sa page d'analyse
replay.back — revient à la sélection préservée
replay.compare identifiant d'un autre replay du classement, ou chaîne vide choisit ou retire la référence, réévaluée avec le score principal
replay.watch — regarde le replay courant au rate enregistré, sans créer de score
replay.share identifiant d'un replay du classement écrit ce score dans un fichier .pvreplay choisi avec la boîte d'enregistrement native (voir docs/game.md)
replay.import — ouvre un fichier .pvreplay avec le sélecteur natif ; le déposer sur la fenêtre fait la même chose
performance.choose identifiant d'un calculateur disponible, ou chaîne vide pour Auto recalcule la performance avec la précision du jugement courant ; les mélanges de modèles sont signalés
library.search texte recherche (appliquée 150 ms après la dernière frappe)
library.filter.edit clé du catalogue ouvre l’éditeur du champ, prérempli si le critère existe déjà
library.filter.field champ (min, max, text, match), texte modifie le brouillon ; match vaut contains ou exact ; une borne vide signifie aucune limite
library.filter.apply — valide et applique le critère ; une erreur conserve le brouillon et la requête précédente
library.filter.cancel — abandonne le brouillon sans changer les résultats
library.filter.remove clé retire explicitement ce critère, même si son fournisseur est indisponible
library.toggle position ouvre le set, ou le referme s'il est ouvert
library.select position, rang sélectionne une difficulté (ouvre son set)
library.collapse — referme le set ouvert
library.up, library.down, library.previousSet, library.nextSet — voir les touches
library.viewport haut, hauteur (px) décalage et hauteur de la liste rendue
library.metrics pas d'un set, pas d'une difficulté (8 à 1000 px) géométrie de la liste rendue
library.import, library.refresh, library.demo — ajouter des dossiers, actualiser, démo sans audio (le bouton « Démo » n'est affiché que bibliothèque vide)
library.visualize — ouvre le visualiseur de map sur le chart sélectionné à la place de la liste, ou le referme ; nécessite le panneau par défaut (un modèle n'a pas de viewport à découper)
navigation.open destination (library, editor, skins, settings, mods) change de page, sauf settings qui ouvre la fenêtre modale sur la dernière catégorie
navigation.settings catégorie (gameplay, controls, audio, video, judgement, scroll, library, interface, mods ; les anciens general, display et hud ouvrent interface, video et interface) ouvre la fenêtre des Réglages au-dessus de la bibliothèque, sur cette catégorie
navigation.drawer — ferme les Réglages s'ils sont ouverts ; sinon ouvre ou ferme le menu Échap
judgement.choose clé d'un jeu jouable choisit ce jeu de jugement
rating.choose clé d'une note enregistrée et visible change le calculateur affiché pour toute la bibliothèque
rating.toggle clé d'une note enregistrée masque ou réaffiche son choix dans le sélecteur, indépendamment du mod qui la fournit
skin.customize — démarre l'éditeur de skin sur le skin en cours (editLayout) ; les Réglages se masquent tant que dure la session
skin.resetCustomization id du skin retire toute la configuration du joueur de ce skin (réglage skinConfig, aucune commande dédiée de l'hôte) ; refusée pendant une session
modOption.set clé de l'élément (<mod>/<élément>), nom de l'option, valeur (booléen, nombre ou texte) change une option joueur d'un élément (editElement avec commit) ; une valeur hors déclaration est refusée sans commande, les nombres sont bornés
modOption.reset clé de l'élément, nom de l'option rétablit sa valeur par défaut déclarée
modElement.reset clé de l'élément rétablit les valeurs par défaut de toutes ses options joueur

invoke refuse un nom inconnu (y compris les noms de Object.prototype), un nombre d'arguments différent ou un argument d'une autre sorte (entier, nombre fini, texte de 1000 caractères au plus, booléen ou nombre fini ou texte pour une valeur d'option, page ou catégorie connue).

Les thèmes et la couche de comportement

pages/Library.svelte (barre d'outils, recherche, état de l'import), components/SongList.svelte (la liste virtuelle, data-pvng-list="sets", pas lus dans les jetons --pv-set-row/-gap et --pv-diff-row/-gap), components/SelectedMap.svelte (panneau de gauche, JudgementPicker, Jouer) lisent stores et appellent act(...) ; App.svelte applique la keymap (DEFAULT_KEYMAP, menu/keys.ts) et le menu Échap suit navigation.drawer.

Le thème choisi (Skins → Interface, réglage uiTheme) est l'attribut data-direction de <html> ; ses jetons --pv-* sont posés par theme/apply.ts à partir de l'événement interfaces que l'hôte publie après validation. Le thème classic garde les styles d'origine des composants ; les autres reçoivent la structure du système de design (theme/ui.css).

Sélecteur de modificateurs (onglet Mods de la map)

L'onglet Mods de la map choisie (à ne pas confondre avec la page Mods du tiroir, qui liste les mods installés avec leurs interrupteurs : elle n'est pas modifiée ici ; une liste groupée avec une icône par mod y serait la suite logique) montre d'abord Rate (compact, sans paragraphe), puis les modificateurs de jeu en tuiles : components/ModPicker.svelte. C'est la seule interface de cet onglet : les modificateurs et leurs paramètres sont ceux que l'hôte déclare, l'interface ne les invente pas.

  • Disposition : les familles sont des blocs côte à côte qui coulent sur la largeur de l'onglet (flex-wrap) : sur une même ligne tant qu'elles tiennent (les trois familles actuelles tiennent sur une ligne à 1400×850, en français aussi), sinon une famille passe à la ligne suivante, sans jamais couper un nom. Les tuiles d'une famille restent sur une ligne (largeur selon le contenu, 84 px au moins, nom en 12 px, aucune troncature) et ne bougent pas quand on sélectionne (le nom ne passe pas en gras ; la place du bouton Réglages est réservée). Flèches : gauche/droite suivent l'ordre de lecture d'une famille à la suivante ; haut/bas vont à la ligne voisine, à la tuile la plus proche en colonne (rien en dessous : sans effet).
  • Familles (petit titre, familles vides omises) : le champ group de l'hôte (longNotes, layout, assist, timing, difficulty, other), dans cet ordre : Longues notes d'abord (No LN et Full LN côte à côte), puis Disposition, Assistances, etc. Un groupe inconnu vient en dernier ; ses textes sont chartgroup.<groupe>.
  • Tuile : une icône lucide, un nom court, un bouton à bascule (aria-pressed) ; sélectionnée, l'icône est sur l'accent du thème et la tuile porte le liseré d'accent de ses plaques. La description et les conflits sont dans l'infobulle commune (use:tooltip) et, pour les lecteurs d'écran, dans aria-describedby. Uniquement des jetons du thème.
  • Icônes : le champ icon de l'hôte est cherché dans la table fermée lib/mod-icons.ts (les 28 icônes lucide de MODIFIER_ICONS de crates/modding/src/modifiers.rs, une liste que mod-icons.test.ts relit dans le source Rust) ; tout autre texte donne la pièce de puzzle, jamais rien d'injecté dans la page.
  • Conflits : conflictsWith de l'hôte, symétrique (l'un des deux côtés suffit). Activer un modificateur désactive ceux avec qui il est en conflit (setGameplayMod) et une annonce polie (role="status") le dit ; une sélection ancienne qui contiendrait les deux affiche les deux tuiles en avertissement (triangle, texte warn), et l'hôte joue sans l'un ni l'autre.
  • Clavier : un seul arrêt de tabulation sur la grille (roving tabindex) ; flèches gauche/droite d'une tuile à l'autre, haut/bas vers la rangée voisine, Début/Fin ; Espace ou Entrée bascule. Le mouvement réduit supprime les transitions.
  • Réglages d'un modificateur : un modificateur dont l'hôte déclare des params reçoit, tant qu'il est sélectionné seulement, un petit bouton-icône « Réglages » posé sur sa tuile (un arrêt de tabulation à part). Il ouvre un petit menu (ChartPopover en role="dialog", ModParamsMenu.svelte) : Échap ou un clic dehors le ferme et rend le focus au bouton ; il est borné par la fenêtre (il défile à l'intérieur s'il est long) et ne déplace rien dans la grille. Le menu est généré du schéma de l'hôte : titres = section, un curseur + une case numérique modifiable (unité à droite) pour un paramètre slider, des boutons segmentés (radiogroup, flèches gauche/droite) pour un choice, seulement les paramètres dont la condition showWhen est vraie (gapMs si l'unité d'écart est fixe, gapPercent si elle est en %), un bouton-icône « Défauts ». Chaque changement s'écrit tout de suite dans le réglage gameplayModParams (même synchronisation que les autres réglages ; l'hôte valide, arrondit et enregistre dans le replay), et une ligne d'état donne le compteur calculé par l'hôte (chartModsPreview, demandé 120 ms après le dernier changement). Rate n'a pas d'options : pas de bouton de réglages.
  • Contrat : GameplayModifierInfo (group, icon, conflictsWith, params: ModParamInfo[], voir docs/modding.md et docs/game.md) ; le simulateur de développement publie le schéma Rust (src/mock/full-ln-params.json, tenu à jour par un test). Les textes viennent des clés chartgroup.* et chartparam.*, le repli est le texte anglais de l'hôte.
  • Tests : lib/mod-picker.test.ts, lib/mod-icons.test.ts, lib/gameplay-mods.test.ts ; tests/mod-picker.spec.ts (tuiles, exclusion, infobulle, menu généré, choix et conditions, compteur, clavier, aucun chevauchement à 1400×850 et 1000×700 en classic, cabinet, macos-light).

Source dans le dépôt du jeu : docs/menus.md