Files
mabibli/MaBibli.Client/Composants/FormulaireLivre.razor
T
mathieuandClaude Opus 5 ad00fd5116 Aligne les thèmes sur le point-virgule des auteurs
Le champ des auteurs se saisit au point-virgule depuis toujours ; celui des
thèmes se saisissait à la virgule. Deux champs voisins du même formulaire ne
doivent pas se saisir de deux façons.

La virgule reste acceptée en repli : c'était l'habitude, et un thème n'en
contient jamais — contrairement à un titre d'article à la une, qui arrive au lot
suivant et pour lequel elle couperait le titre en deux. D'où le paramètre de
`ListeSaisie` plutôt qu'une règle unique.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:24:57 +02:00

217 lines
8.9 KiB
Plaintext

@*
Formulaire d'une fiche livre, partagé par l'ajout (ISBN ou manuel) et l'édition.
Le pré-remplissage par ISBN est une AIDE, pas une contrainte : tous les champs restent
modifiables, et une fiche incomplète peut toujours être complétée à la main (CLAUDE.md).
*@
<div class="formulaire">
@if (!string.IsNullOrEmpty(Saisie.CoverUrl) || !string.IsNullOrWhiteSpace(Saisie.Titre))
{
<div class="formulaire-apercu">
<Couverture Url="@Saisie.CoverUrl" Titre="@Saisie.Titre" Classe="couverture-moyenne" />
</div>
}
<label class="champ">
<span class="champ-libelle">Titre <span class="obligatoire" aria-hidden="true">*</span></span>
<input class="champ-saisie" type="text" @bind="Saisie.Titre" @bind:event="oninput"
placeholder="Titre du livre" autocomplete="off" />
</label>
@*
Un seul champ, plusieurs auteurs : le point-virgule les sépare. La virgule est déjà
prise par la forme bibliographique « Zola, Émile », qu'on doit pouvoir taper telle quelle.
Le serveur rapproche ensuite les variantes sûres d'un auteur déjà connu.
*@
<label class="champ">
<span class="champ-libelle">
Auteur<span class="champ-aide"> (plusieurs : séparez par « ; »)</span>
</span>
<input class="champ-saisie" type="text" @bind="Saisie.Auteur" @bind:event="oninput"
placeholder="Prénom Nom ; Prénom Nom" autocomplete="off" />
</label>
@*
Les rôles n'apparaissent qu'à partir de DEUX auteurs, et c'est délibéré : un roman n'a
qu'un signataire dont le rôle ne se pose pas, et une ligne de boutons sous chaque saisie
n'apprendrait rien. La question « qui écrit, qui dessine ? » ne se pose qu'à plusieurs.
Le défaut reste « non précisé » : on ne remplit pas une case à la place de l'utilisateur.
*@
@if (Saisie.Auteurs.Count > 1)
{
<fieldset class="champ">
<legend class="champ-libelle">
Rôles<span class="champ-aide"> (facultatif — utile pour une BD)</span>
</legend>
@foreach (var (auteur, index) in Saisie.Auteurs.Select((a, i) => (a, i)))
{
<div class="ligne-role" @key="index">
<span class="nom-role">@auteur.Nom</span>
<div class="segments">
@foreach (var role in Roles)
{
<button type="button"
class="segment @(auteur.Role == role ? "segment-actif" : null)"
@onclick="() => PoserRole(index, role)">
@Libelles.RoleAuteur(role)
</button>
}
</div>
</div>
}
</fieldset>
}
<label class="champ">
<span class="champ-libelle">Éditeur</span>
<input class="champ-saisie" type="text" @bind="Saisie.Editeur" @bind:event="oninput"
autocomplete="off" />
</label>
<label class="champ">
@* Point-virgule, comme le champ des auteurs juste au-dessus : trois champs voisins du
même formulaire ne doivent pas se saisir de trois façons. La virgule reste acceptée —
un thème n'en contient jamais, contrairement à un titre d'article à la une. *@
<span class="champ-libelle">
Thèmes <span class="champ-aide">(séparés par des points-virgules)</span>
</span>
<input class="champ-saisie" type="text" @bind="Saisie.ThemesTexte" @bind:event="oninput"
placeholder="Fantasy ; aventure ; space opera" autocomplete="off" />
</label>
<label class="champ">
<span class="champ-libelle">ISBN <span class="champ-aide">(facultatif)</span></span>
<input class="champ-saisie" type="text" inputmode="numeric" @bind="Saisie.Isbn" @bind:event="oninput"
placeholder="978…" autocomplete="off" />
</label>
@*
Le format n'est plus affiché dans les listes que pour les ebooks : un livre y est donc
physique par défaut. Cette valeur DOIT rester présélectionnée ici, sinon on créerait
des fiches sans format en croyant que « rien d'affiché = physique ».
C'est le cas sans code particulier : Format.Physique vaut 0, valeur par défaut de
l'énumération, et le segment correspondant est donc actif dès l'ouverture.
*@
<fieldset class="champ">
<legend class="champ-libelle">
Format<span class="champ-aide"> (physique par défaut)</span>
</legend>
<div class="segments">
@foreach (var format in Formats)
{
<button type="button"
class="segment @(Saisie.Format == format ? "segment-actif" : null)"
@onclick="() => Saisie.Format = format">
@Libelles.Format(format)
</button>
}
</div>
</fieldset>
@*
⚠️ Le défaut est « non précisé », PAS « roman » : on ne remplit pas une case à la place
de l'utilisateur. C'est aussi ce qui rend l'étiquette honnête dans les listes — elle ne
s'affiche que lorsqu'un type a été choisi.
*@
<fieldset class="champ">
<legend class="champ-libelle">
Type<span class="champ-aide"> (facultatif)</span>
</legend>
<div class="segments">
@foreach (var type in Types)
{
<button type="button"
class="segment @Libelles.ClasseTypeDocument(type) @(Saisie.TypeDocument == type ? "segment-actif" : null)"
@onclick="() => Saisie.TypeDocument = type">
@Libelles.TypeDocument(type)
</button>
}
</div>
</fieldset>
<fieldset class="champ">
<legend class="champ-libelle">
Statut de lecture<span class="champ-aide"> (le vôtre)</span>
</legend>
<ChoixStatut Valeur="Saisie.Statut" ValeurChanged="s => Saisie.Statut = s" />
</fieldset>
<label class="champ">
<span class="champ-libelle">URL de couverture</span>
<input class="champ-saisie" type="url" @bind="Saisie.CoverUrl" @bind:event="oninput"
placeholder="https://…" autocomplete="off" />
</label>
@if (!string.IsNullOrEmpty(Erreur))
{
<p class="message-erreur" role="alert">@Erreur</p>
}
@* Un enregistrement impossible se dit AVANT le clic : un bouton qui échoue en silence est
pire que pas de bouton du tout (CLAUDE.md, stratégie hors-ligne). *@
@if (!string.IsNullOrEmpty(MessageBlocage))
{
<p class="message-avertissement" role="status">@MessageBlocage</p>
}
<div class="actions-formulaire">
<button type="button" class="bouton bouton-principal"
disabled="@(EnCours || Bloque || string.IsNullOrWhiteSpace(Saisie.Titre))"
title="@MessageBlocage"
@onclick="Valider">
@(EnCours ? "Enregistrement…" : LibelleValidation)
</button>
@if (OnAnnuler.HasDelegate)
{
<button type="button" class="bouton bouton-discret" disabled="@EnCours"
@onclick="OnAnnuler">
Annuler
</button>
}
</div>
</div>
@code {
private static readonly Format[] Formats = Enum.GetValues<Format>();
private static readonly TypeDocument[] Types = Enum.GetValues<TypeDocument>();
private static readonly RoleAuteur[] Roles = Enum.GetValues<RoleAuteur>();
/// <summary>
/// Pose le rôle d'un auteur.
/// </summary>
/// <remarks>
/// <see cref="AuteurSaisi"/> est un record : on remplace l'entrée plutôt que de la muter,
/// et la liste reste celle que la propriété <c>Auteur</c> saura relire pour préserver les
/// rôles à la frappe suivante.
/// </remarks>
private void PoserRole(int index, RoleAuteur role) =>
Saisie.Auteurs[index] = Saisie.Auteurs[index] with { Role = role };
[Parameter, EditorRequired] public EnregistrementLivre Saisie { get; set; } = new();
[Parameter] public string LibelleValidation { get; set; } = "Enregistrer";
[Parameter] public string? Erreur { get; set; }
[Parameter] public bool EnCours { get; set; }
/// <summary>
/// Raison pour laquelle l'enregistrement est impossible (typiquement l'absence de réseau).
/// Renseignée, elle désactive la validation et s'affiche : la saisie reste lisible, mais
/// l'utilisateur sait pourquoi il ne peut pas valider.
/// </summary>
[Parameter] public string? MessageBlocage { get; set; }
private bool Bloque => !string.IsNullOrEmpty(MessageBlocage);
[Parameter] public EventCallback OnValider { get; set; }
[Parameter] public EventCallback OnAnnuler { get; set; }
private Task Valider() => OnValider.InvokeAsync();
}