Cinq retours d'IDEES.md, tous pensés pour le mobile d'abord.
Fiche du livre en deux modes, portés par deux routes : « /livres/{id} »
consulte, « /livres/{id}/edition » modifie. Ouvrir une fiche depuis le
catalogue ne place plus un formulaire vivant sous le pouce. Mettre le mode
dans l'adresse fait du retour navigateur une sortie d'édition naturelle.
Piège rencontré : les deux routes visent le même composant avec le même Id,
donc aucun paramètre ne change et le routeur ne redessine pas — l'URL passait
à « /edition » sans que l'écran bascule, alors qu'un chargement direct de
cette URL marchait, ce qui masque le défaut. Le composant s'abonne donc à
LocationChanged.
Restent hors « Éditer », parce que ce ne sont pas des modifications de la
fiche : le statut de lecture (action la plus fréquente, et personnelle) et
les prêts (gestes du quotidien, donnée du foyer). La suppression, elle, est
réservée au mode édition.
Dans la liste, le statut devient une pastille en lecture seule sur la ligne
des étiquettes ; les trois boutons actionnables par carte partaient trop
facilement en faisant défiler. Le format ne s'affiche plus que pour les
ebooks, le physique étant le cas par défaut — le formulaire le
présélectionne déjà, l'énumération valant 0.
Couverture agrandissable au clic, fermeture au clic extérieur ou par Échap
(le calque est focalisé à l'ouverture, sans quoi Échap resterait sans effet).
Le substitut à initiale n'est jamais cliquable : le déclencheur n'existe que
s'il y a une image.
Le regroupement par auteur existait déjà (table Auteur, page /auteurs, filtre
« ?auteur= ») ; y manquaient les liens depuis la fiche et les styles de la
page auteurs, jamais écrits — la liste sortait en puces brutes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107 lines
3.5 KiB
Plaintext
107 lines
3.5 KiB
Plaintext
@*
|
|
Couverture d'un livre, avec substitut.
|
|
|
|
Les URL de couverture OpenLibrary renvoient des 502 intermittents (mesuré en phase 2 :
|
|
200, 200, puis 502 sur le même ISBN). On ne valide donc JAMAIS l'URL par un appel préalable :
|
|
on l'affiche, et si l'image casse, l'attribut onerror la masque et laisse apparaître le
|
|
substitut qui se trouve déjà dessous. Aucun aller-retour serveur, aucun état C# à gérer.
|
|
|
|
@key sur l'img : si l'URL change, Blazor recrée l'élément au lieu de réutiliser celui que
|
|
le onerror a marqué comme cassé.
|
|
|
|
Agrandissement (IDEES.md) : seulement quand il y a une URL. Sans couverture, il n'y a rien
|
|
à agrandir — le substitut à initiale reste un simple décor, jamais un bouton.
|
|
*@
|
|
<div class="couverture @Classe">
|
|
<span class="couverture-substitut" aria-hidden="true">@Initiale</span>
|
|
|
|
@if (ADesPixels)
|
|
{
|
|
@if (Agrandissable)
|
|
{
|
|
@* Un vrai <button> et pas une <div> cliquable : cible tactile, focus clavier,
|
|
et activation à l'Entrée sans une ligne de code. *@
|
|
<button type="button" class="couverture-declencheur"
|
|
aria-label="Agrandir la couverture de @Titre"
|
|
@onclick="Ouvrir">
|
|
<img @key="Url"
|
|
class="couverture-image"
|
|
src="@Url"
|
|
alt="Couverture de @Titre"
|
|
loading="lazy"
|
|
onerror="this.classList.add('image-cassee')" />
|
|
</button>
|
|
}
|
|
else
|
|
{
|
|
<img @key="Url"
|
|
class="couverture-image"
|
|
src="@Url"
|
|
alt="Couverture de @Titre"
|
|
loading="lazy"
|
|
onerror="this.classList.add('image-cassee')" />
|
|
}
|
|
}
|
|
</div>
|
|
|
|
@if (_agrandie)
|
|
{
|
|
@* Fermeture au clic extérieur (le clic sur l'image ne remonte pas) et à Échap.
|
|
Le tabindex rend le calque focusable : sans focus, aucun keydown ne lui parviendrait. *@
|
|
<div class="calque-couverture" role="dialog" aria-modal="true"
|
|
aria-label="Couverture de @Titre"
|
|
tabindex="-1" @ref="_calque"
|
|
@onclick="Fermer" @onkeydown="SurToucheAsync">
|
|
<img class="calque-image" src="@Url" alt="Couverture de @Titre"
|
|
@onclick:stopPropagation="true"
|
|
onerror="this.classList.add('image-cassee')" />
|
|
|
|
<button type="button" class="calque-fermer" aria-label="Fermer">✕</button>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
[Parameter] public string? Url { get; set; }
|
|
|
|
[Parameter] public string? Titre { get; set; }
|
|
|
|
[Parameter] public string? Classe { get; set; }
|
|
|
|
/// <summary>Le clic sur l'image l'affiche en grand. Sans effet s'il n'y a pas de couverture.</summary>
|
|
[Parameter] public bool Agrandissable { get; set; }
|
|
|
|
private bool _agrandie;
|
|
private bool _aFocaliser;
|
|
private ElementReference _calque;
|
|
|
|
private bool ADesPixels => !string.IsNullOrWhiteSpace(Url);
|
|
|
|
private string Initiale =>
|
|
string.IsNullOrWhiteSpace(Titre) ? "?" : Titre.Trim()[..1].ToUpperInvariant();
|
|
|
|
private void Ouvrir()
|
|
{
|
|
_agrandie = true;
|
|
_aFocaliser = true;
|
|
}
|
|
|
|
private void Fermer() => _agrandie = false;
|
|
|
|
private void SurToucheAsync(KeyboardEventArgs e)
|
|
{
|
|
if (e.Key is "Escape" or "Esc")
|
|
{
|
|
Fermer();
|
|
}
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool premierRendu)
|
|
{
|
|
if (_aFocaliser)
|
|
{
|
|
_aFocaliser = false;
|
|
await _calque.FocusAsync();
|
|
}
|
|
}
|
|
}
|