Ouvrir(revue.Id)">
+ @revue.Titre
@@ -83,6 +96,8 @@ else
@code {
private IReadOnlyList? _revues;
+ private void Ouvrir(int id) => Navigation.NavigateTo($"revues/{id}");
+
private static string Quand(NumeroRevueDto numero) =>
numero.DateParution is { } date ? $"({date.ToLocalTime():MM/yyyy})" : string.Empty;
diff --git a/MaBibli.Client/Pages/Serie.razor b/MaBibli.Client/Pages/Serie.razor
index ac7a7c2..1f3fb54 100644
--- a/MaBibli.Client/Pages/Serie.razor
+++ b/MaBibli.Client/Pages/Serie.razor
@@ -26,19 +26,52 @@ else
}
}
- @_serie.Titre
+ @*
+ Titre, avancement et actions sur la MÊME ligne (lot T) : c'est la disposition déjà tenue
+ par la bibliographie et la liste des auteurs, et elle rend les deux actions visibles sans
+ descendre au bas d'une saga de vingt tomes.
-
- @if (_serie.Elements.Count == 0)
+ ⚠️ « Changer l'ordre » n'apparaît qu'à partir de DEUX tomes ou deux sous-séries : un
+ bouton toujours présent mais inerte serait un recul. Et les deux écrans restent distincts
+ — « Modifier » touche à la fiche (nom, cycle, suppression), « Changer l'ordre » à la
+ place des tomes, dont les flèches ne vivent que là.
+ *@
+
+
+
@_serie.Titre
+
+
+ @if (_serie.Elements.Count == 0)
+ {
+ Aucun tome recensé. Ajoutez ceux que vous possédez, et ceux qui vous manquent.
+ }
+ else
+ {
+ var total = Total(_serie);
+ @total.Possedes sur @total.Total tomes dans la bibliothèque.
+ }
+
+
+
+ @if (ModeConsultation)
{
-
Aucun tome recensé. Ajoutez ceux que vous possédez, et ceux qui vous manquent.
+
+ @if (Reseau.EnLigne)
+ {
+ @if (_serie.Elements.Count > 1 || _filles.Count > 1)
+ {
+
Changer l'ordre
+ }
+
Modifier
+ }
+ else
+ {
+
Modifier
+ }
+
}
- else
- {
- var total = Total(_serie);
-
@total.Possedes sur @total.Total tomes dans la bibliothèque.
- }
-
+
@if (ModeOrdre)
{
@@ -439,27 +472,6 @@ else
Terminé
}
- else if (ModeConsultation)
- {
-
- @if (Reseau.EnLigne)
- {
- @* Deux écrans distincts, et ce n'est pas une subtilité : « Modifier » touche à la
- fiche (nom, cycle, suppression), « Changer l'ordre » à la place des tomes. Les
- mêmes flèches offertes en consultation se déclenchaient au défilement. *@
- @if (_serie.Elements.Count > 1 || _filles.Count > 1)
- {
- Changer l'ordre
- }
- Modifier
- }
- else
- {
- Modifier
- }
-
- }
}
@code {
diff --git a/MaBibli.Client/Services/RemonteeRoutes.cs b/MaBibli.Client/Services/RemonteeRoutes.cs
new file mode 100644
index 0000000..926bf09
--- /dev/null
+++ b/MaBibli.Client/Services/RemonteeRoutes.cs
@@ -0,0 +1,161 @@
+namespace MaBibli.Client.Services;
+
+///
+/// Parenté des routes de l'application : à quel écran remonte la flèche de retour du bandeau.
+///
+///
+///
+/// ⚠️ Ceci renverse la décision actée le 2026-08-20 (« le retour passe par l'historique du
+/// navigateur, jamais par une destination calculée »). Le motif d'alors reste vrai — un même
+/// écran s'atteint par plusieurs chemins — mais l'historique remonte aussi les
+/// allers-retours (filtre, ordre, édition) : on cliquait cinq fois sans quitter le même
+/// écran. Une remontée d'un cran de route répond à « où suis-je ? », qui est la question posée.
+///
+///
+/// La table est explicite , et non un découpage naïf de l'URL : toutes les routes n'ont pas
+/// la forme d'une arborescence (/souhaits/ordre remonte à /souhaits , mais
+/// /auteurs/{id}/bibliographie remonte à /auteurs — la fiche d'un auteur n'existe
+/// pas). Une route qui apparaît dans l'application doit apparaître ici.
+///
+///
+/// ⚠️ Le retour ne sort jamais de l'application : la fonction rend toujours un chemin
+/// interne, et la racine d'une branche rend la destination de menu correspondante — le catalogue
+/// en dernier ressort. C'est ce qui remplace le test de history.length qui vivait en
+/// JavaScript, et qui ne disait pas ce qu'on croyait dans une PWA standalone : la pile
+/// d'une session y contient aussi ce qui précède l'application.
+///
+///
+public static class RemonteeRoutes
+{
+ /// Le catalogue est la racine : son parent est lui-même, on ne remonte pas plus haut.
+ public const string Racine = "/";
+
+ ///
+ /// Table de parenté, dans l'ordre de lecture. {id} représente un segment numérique.
+ ///
+ ///
+ /// La première ligne dont le modèle correspond gagne : les modèles les plus longs sont donc
+ /// écrits avant ceux dont ils sont un prolongement.
+ ///
+ private static readonly (string Modele, string Parent)[] Parents =
+ [
+ // Fiche livre : l'édition retombe sur la consultation, la consultation sur le catalogue.
+ ("/livres/{id}/edition", "/livres/{id}"),
+ ("/livres/{id}", Racine),
+
+ // Séries : les deux écrans de modification retombent sur la consultation de la série.
+ ("/series/{id}/edition", "/series/{id}"),
+ ("/series/{id}/ordre", "/series/{id}"),
+ ("/series/{id}", "/series"),
+ ("/series", Racine),
+
+ // Envies : l'ordre et l'ajout sont deux écrans de la même liste.
+ ("/souhaits/ordre", "/souhaits"),
+ ("/souhaits/ajout", "/souhaits"),
+ ("/souhaits", Racine),
+
+ // Ajout d'un ouvrage : on y entre depuis le catalogue comme depuis les revues, mais le
+ // catalogue est la destination de menu de ce qu'on y saisit.
+ ("/ajout/manuel", Racine),
+ ("/ajout/isbn", Racine),
+ ("/ajout", Racine),
+
+ // Revues. ⚠️ « /revues/ajout » est écrit AVANT « /revues/{id} » : « ajout » n'est pas un
+ // identifiant, mais l'ordre de lecture est ce qui le garantit sans ambiguïté.
+ ("/revues/ajout", "/revues"),
+ ("/revues/{id}", "/revues"),
+ ("/revues", Racine),
+
+ // La bibliographie remonte à la LISTE des auteurs : il n'existe pas de fiche auteur.
+ ("/auteurs/{id}/bibliographie", "/auteurs"),
+ ("/auteurs", Racine),
+
+ ("/prets", Racine),
+ ("/not-found", Racine),
+ ];
+
+ ///
+ /// Écran d'où l'on vient hiérarchiquement, à partir d'un chemin d'application.
+ ///
+ ///
+ /// Chemin absolu ou relatif à la base, avec ou sans requête (?auteur=3 ) ni fragment.
+ ///
+ /// Un chemin interne, toujours ; jamais null , jamais une URL absolue.
+ public static string Parent(string? chemin)
+ {
+ var segments = Segments(chemin);
+
+ if (segments.Length == 0)
+ {
+ return Racine;
+ }
+
+ foreach (var (modele, parent) in Parents)
+ {
+ if (Correspond(Segments(modele), segments))
+ {
+ return Rendre(parent, segments);
+ }
+ }
+
+ // Route inconnue : on retombe sur la destination de menu de sa branche, plutôt que sur
+ // un chemin deviné. Une route ajoutée sans sa ligne de table reste ainsi utilisable —
+ // mais elle doit être ajoutée : c'est un repli, pas le mécanisme.
+ return Parents.FirstOrDefault(p => p.Modele == $"/{segments[0]}").Parent is not null
+ ? $"/{segments[0]}"
+ : Racine;
+ }
+
+ /// Découpe en segments non vides, requête et fragment retirés.
+ private static string[] Segments(string? chemin)
+ {
+ if (string.IsNullOrWhiteSpace(chemin))
+ {
+ return [];
+ }
+
+ var utile = chemin.Split('?', '#')[0];
+
+ return utile.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ }
+
+ private static bool Correspond(string[] modele, string[] segments)
+ {
+ if (modele.Length != segments.Length)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < modele.Length; i++)
+ {
+ var attendu = modele[i];
+
+ var ok = attendu == "{id}"
+ ? int.TryParse(segments[i], out _)
+ : string.Equals(attendu, segments[i], StringComparison.OrdinalIgnoreCase);
+
+ if (!ok)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// Remplace {id} du parent par l'identifiant lu dans le chemin d'origine.
+ private static string Rendre(string parent, string[] segments)
+ {
+ if (!parent.Contains("{id}", StringComparison.Ordinal))
+ {
+ return parent;
+ }
+
+ var identifiant = segments.FirstOrDefault(s => int.TryParse(s, out _));
+
+ // Sans identifiant lisible, on ne fabrique pas une route bancale : la racine est sûre.
+ return identifiant is null
+ ? Racine
+ : parent.Replace("{id}", identifiant, StringComparison.Ordinal);
+ }
+}
diff --git a/MaBibli.Client/wwwroot/css/app.css b/MaBibli.Client/wwwroot/css/app.css
index eb04867..2460276 100644
--- a/MaBibli.Client/wwwroot/css/app.css
+++ b/MaBibli.Client/wwwroot/css/app.css
@@ -139,6 +139,38 @@ body {
margin: 0 0 0.75rem;
}
+/*
+ Ligne d'en-tête d'une fiche : le texte à gauche, les actions groupées à DROITE (lot T).
+
+ C'est la disposition déjà tenue par la bibliographie et la liste des auteurs. Reléguées en
+ bas de l'écran, les actions d'une saga de vingt tomes ne se découvraient qu'après avoir
+ déroulé toute la liste.
+
+ ⚠️ `min-width: 0` sur la colonne de texte : sans lui, un titre long refuse de rétrécir et
+ pousse les actions hors de l'écran — le piège déjà rencontré au lot M1.
+*/
+.entete-fiche {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.entete-fiche-texte {
+ flex: 1 1 12rem;
+ min-width: 0;
+}
+
+.entete-fiche-actions {
+ flex: 0 0 auto;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 0.4rem;
+}
+
.message-discret {
color: var(--mb-texte-doux);
font-size: 0.9rem;
@@ -1190,6 +1222,18 @@ body {
border-bottom: 1px solid var(--mb-bord);
}
+/* Ligne entièrement cliquable (lot V) : le curseur doit le dire, sans quoi seul le titre a
+ l'air de mener quelque part. Le survol marque la ligne visée, utile à la souris comme au
+ clavier lorsque le lien de titre prend le focus. */
+.carte-cliquable {
+ cursor: pointer;
+}
+
+.carte-cliquable:hover,
+.carte-cliquable:focus-within {
+ background: var(--mb-fond-doux);
+}
+
.liste-sous-series {
margin: 0.35rem 0 0 1rem;
font-size: 0.9rem;
@@ -1321,6 +1365,21 @@ body {
autres : c'est tout l'intérêt du gabarit retenu le 2026-08-20.
------------------------------------------------------------------ */
+/*
+ Bandeau et menu collent ENSEMBLE en haut de l'écran (lot R) : sur PC, la rangée des six
+ destinations défilait avec la page et devenait inatteignable dès qu'on descendait dans une
+ longue liste.
+
+ ⚠️ Un seul conteneur collant, et non deux éléments collants superposés : ceux-ci auraient
+ obligé à écrire en dur la hauteur du bandeau dans le `top` du menu, hauteur qui varie avec
+ la pastille hors-ligne et le nom d'utilisateur.
+*/
+.entete {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
.menu {
display: none;
flex-direction: column;
@@ -1328,8 +1387,66 @@ body {
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}
+/*
+ Téléphone : le menu déployé occupe l'écran ENTIER.
+
+ Déployé sous le bandeau, il partageait l'écran avec la liste qu'on quittait, et l'on
+ choisissait sa destination au milieu d'autre chose. En plein écran, les six destinations se
+ lisent d'un coup d'œil.
+
+ ⚠️ Le calque garde une porte de sortie explicite — la croix ci-dessous, Échap, et le clic
+ hors des liens (posés dans MainLayout.razor), en plus de la fermeture sur LocationChanged.
+ Sans elles, un plein écran est un piège.
+*/
.menu-ouvert {
display: flex;
+ position: fixed;
+ inset: 0;
+ z-index: 50;
+ overflow-y: auto;
+ padding-bottom: 2rem;
+ /* Le calque prend le focus à l'ouverture : sans cela Échap ne lui parviendrait pas. Le
+ contour du navigateur n'apprend rien ici, la croix étant visible. */
+ outline: none;
+}
+
+.menu-entete {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+ padding: 0.6rem 0.75rem;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.15);
+}
+
+.menu-titre {
+ color: #e8edf3;
+ font-size: 0.85rem;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+/* Cible tactile de 2.25rem, comme les boutons du bandeau : on la touche au pouce. */
+.menu-fermer {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.25rem;
+ height: 2.25rem;
+ padding: 0;
+ font-size: 1.15rem;
+ line-height: 1;
+ color: #fff;
+ background: transparent;
+ border: 1px solid rgba(255, 255, 255, 0.35);
+ border-radius: 6px;
+ cursor: pointer;
+}
+
+.menu-fermer:hover,
+.menu-fermer:focus-visible {
+ background: rgba(255, 255, 255, 0.15);
}
.menu-lien {
@@ -1373,6 +1490,21 @@ body {
padding: 0 0.5rem;
}
+ /*
+ ⚠️ Le plein écran est ANNULÉ ici : sans cela, un menu ouvert au doigt puis un
+ agrandissement de la fenêtre laisserait un calque plein écran sur un PC, sans bascule
+ pour le refermer (elle est masquée à partir de cette largeur).
+ */
+ .menu-ouvert {
+ position: static;
+ overflow-y: visible;
+ padding-bottom: 0;
+ }
+
+ .menu-entete {
+ display: none;
+ }
+
.menu-lien {
padding: 0.6rem 0.9rem;
border-left: 0;
diff --git a/MaBibli.Client/wwwroot/index.html b/MaBibli.Client/wwwroot/index.html
index 6a37ac1..3a2252a 100644
--- a/MaBibli.Client/wwwroot/index.html
+++ b/MaBibli.Client/wwwroot/index.html
@@ -39,9 +39,6 @@
navigator.serviceWorker (absent hors contexte sécurisé), journalise un échec au lieu de
l'avaler, et propose la mise à jour quand une nouvelle version est prête. -->
-
-