API - CRUD complet sur /api/livres : liste filtrable (format, statut) et cherchable (titre, auteur), consultation, creation, modification, changement de statut isole, suppression. - Aucune lecture ne filtre sur AjoutePar : la bibliotheque est commune au foyer. Le critere n'existe meme pas dans CritereLivres, pour qu'on ne puisse pas s'en servir par inadvertance. Deux tests verrouillent l'invariant, dont un par reflexion sur le type des criteres. - AjoutePar est renseigne par le serveur a la creation, jamais par le client, et n'est pas reecrit a l'edition (pas plus que DateAjout). - Identite lue dans les en-tetes SSOwat (YNH_USER, YNH_USER_EMAIL, YNH_USER_FULLNAME), avec repli sur un utilisateur simule configure en developpement uniquement. Sans en-tete ni simulation, l'identite reste inconnue plutot qu'inventee. Expose par GET /api/moi. - Migration appliquee au demarrage : pas d'etape manuelle a l'installation. Interface Blazor, pensee mobile d'abord - Catalogue unique (physiques et numeriques melanges, distingues par le format) avec recherche, filtres et changement de statut directement dans la liste. - Ajout par ISBN : quand la cascade rend plusieurs notices, un ecran de choix les presente en mettant en avant editeur et annee, seuls elements qui les departagent. Le formulaire reste ensuite entierement modifiable. - Ajout manuel, edition, suppression avec confirmation explicite. - Couvertures : l'URL OpenLibrary n'est jamais validee par un appel prealable (502 intermittents mesures en phase 2) ; un attribut onerror masque l'image cassee et laisse apparaitre un substitut. Decisions prises la ou CLAUDE.md etait muet - Tri alphabetique par titre. - Un ISBN saisi doit etre valide, mais reste facultatif. - Ecran de choix affiche seulement a partir de deux candidats. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
452 lines
10 KiB
CSS
452 lines
10 KiB
CSS
html, body {
|
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
}
|
|
|
|
h1:focus {
|
|
outline: none;
|
|
}
|
|
|
|
a, .btn-link {
|
|
color: #0071c1;
|
|
}
|
|
|
|
.btn-primary {
|
|
color: #fff;
|
|
background-color: #1b6ec2;
|
|
border-color: #1861ac;
|
|
}
|
|
|
|
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
|
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
|
}
|
|
|
|
.content {
|
|
padding-top: 1.1rem;
|
|
}
|
|
|
|
.valid.modified:not([type=checkbox]) {
|
|
outline: 1px solid #26b050;
|
|
}
|
|
|
|
.invalid {
|
|
outline: 1px solid red;
|
|
}
|
|
|
|
.validation-message {
|
|
color: red;
|
|
}
|
|
|
|
#blazor-error-ui {
|
|
color-scheme: light only;
|
|
background: lightyellow;
|
|
bottom: 0;
|
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
|
box-sizing: border-box;
|
|
display: none;
|
|
left: 0;
|
|
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
|
position: fixed;
|
|
width: 100%;
|
|
z-index: 1000;
|
|
}
|
|
|
|
#blazor-error-ui .dismiss {
|
|
cursor: pointer;
|
|
position: absolute;
|
|
right: 0.75rem;
|
|
top: 0.5rem;
|
|
}
|
|
|
|
.blazor-error-boundary {
|
|
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
|
padding: 1rem 1rem 1rem 3.7rem;
|
|
color: white;
|
|
}
|
|
|
|
.blazor-error-boundary::after {
|
|
content: "An error has occurred."
|
|
}
|
|
|
|
.loading-progress {
|
|
position: absolute;
|
|
display: block;
|
|
width: 8rem;
|
|
height: 8rem;
|
|
inset: 20vh 0 auto 0;
|
|
margin: 0 auto 0 auto;
|
|
}
|
|
|
|
.loading-progress circle {
|
|
fill: none;
|
|
stroke: #e0e0e0;
|
|
stroke-width: 0.6rem;
|
|
transform-origin: 50% 50%;
|
|
transform: rotate(-90deg);
|
|
}
|
|
|
|
.loading-progress circle:last-child {
|
|
stroke: #1b6ec2;
|
|
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
|
transition: stroke-dasharray 0.05s ease-in-out;
|
|
}
|
|
|
|
.loading-progress-text {
|
|
position: absolute;
|
|
text-align: center;
|
|
font-weight: bold;
|
|
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
|
}
|
|
|
|
.loading-progress-text:after {
|
|
content: var(--blazor-load-percentage-text, "Loading");
|
|
}
|
|
|
|
code {
|
|
color: #c02d76;
|
|
}
|
|
|
|
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
|
color: var(--bs-secondary-color);
|
|
text-align: end;
|
|
}
|
|
|
|
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
|
text-align: start;
|
|
}
|
|
/* ------------------------------------------------------------------
|
|
MaBibli — interface du catalogue (phase 3)
|
|
|
|
Conçue mobile d'abord : une seule colonne, cibles tactiles d'au moins
|
|
2,75 rem, aucune règle de largeur en dessous du point de rupture.
|
|
Le PC n'ajoute qu'une largeur maximale et une grille de cartes.
|
|
------------------------------------------------------------------ */
|
|
|
|
:root {
|
|
--mb-accent: #1b3a5c;
|
|
--mb-bord: #dcdcdc;
|
|
--mb-fond-doux: #f4f5f7;
|
|
--mb-texte-doux: #5c6470;
|
|
--mb-danger: #a4242c;
|
|
}
|
|
|
|
body {
|
|
background: #fff;
|
|
color: #1d2129;
|
|
}
|
|
|
|
.titre-page {
|
|
font-size: 1.4rem;
|
|
margin: 0 0 0.75rem;
|
|
}
|
|
|
|
.message-discret {
|
|
color: var(--mb-texte-doux);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.message-erreur {
|
|
background: #fdecec;
|
|
border-left: 3px solid var(--mb-danger);
|
|
color: #7a1a20;
|
|
padding: 0.6rem 0.75rem;
|
|
border-radius: 4px;
|
|
margin: 0.75rem 0;
|
|
}
|
|
|
|
.message-avertissement {
|
|
background: #fff8e1;
|
|
border-left: 3px solid #c99700;
|
|
color: #6b5200;
|
|
padding: 0.6rem 0.75rem;
|
|
border-radius: 4px;
|
|
margin: 0.5rem 0;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.compteur {
|
|
color: var(--mb-texte-doux);
|
|
font-size: 0.85rem;
|
|
margin: 0.75rem 0 0.25rem;
|
|
}
|
|
|
|
/* --- Champs de saisie --- */
|
|
|
|
.barre-recherche {
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.champ {
|
|
display: block;
|
|
margin-bottom: 0.9rem;
|
|
border: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.champ-libelle {
|
|
display: block;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.3rem;
|
|
}
|
|
|
|
.champ-aide,
|
|
.obligatoire {
|
|
font-weight: 400;
|
|
color: var(--mb-texte-doux);
|
|
}
|
|
|
|
.champ-saisie {
|
|
width: 100%;
|
|
/* 16px minimum : en dessous, iOS zoome au focus. */
|
|
font-size: 1rem;
|
|
padding: 0.7rem 0.75rem;
|
|
border: 1px solid var(--mb-bord);
|
|
border-radius: 8px;
|
|
background: #fff;
|
|
}
|
|
|
|
.champ-saisie:focus {
|
|
outline: 2px solid var(--mb-accent);
|
|
outline-offset: -1px;
|
|
}
|
|
|
|
/* --- Segments (filtres, format, statut) --- */
|
|
|
|
.segments {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.35rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.segment {
|
|
flex: 1 1 auto;
|
|
min-height: 2.5rem;
|
|
padding: 0.4rem 0.7rem;
|
|
font-size: 0.85rem;
|
|
border: 1px solid var(--mb-bord);
|
|
border-radius: 999px;
|
|
background: #fff;
|
|
color: #1d2129;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.segment-actif {
|
|
background: var(--mb-accent);
|
|
border-color: var(--mb-accent);
|
|
color: #fff;
|
|
}
|
|
|
|
.segments-compacts .segment {
|
|
min-height: 2.2rem;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.segment.statut-a-lire.segment-actif { background: #6b4fa8; border-color: #6b4fa8; }
|
|
.segment.statut-en-cours.segment-actif { background: #b06b00; border-color: #b06b00; }
|
|
.segment.statut-lu.segment-actif { background: #1f7a44; border-color: #1f7a44; }
|
|
|
|
/* --- Boutons --- */
|
|
|
|
.bouton {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 2.75rem;
|
|
padding: 0.6rem 1rem;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--mb-bord);
|
|
background: #fff;
|
|
color: #1d2129;
|
|
font-size: 0.95rem;
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.bouton:disabled {
|
|
opacity: 0.55;
|
|
cursor: default;
|
|
}
|
|
|
|
.bouton-principal {
|
|
background: var(--mb-accent);
|
|
border-color: var(--mb-accent);
|
|
color: #fff;
|
|
}
|
|
|
|
.bouton-discret {
|
|
background: var(--mb-fond-doux);
|
|
}
|
|
|
|
.bouton-danger {
|
|
background: var(--mb-danger);
|
|
border-color: var(--mb-danger);
|
|
color: #fff;
|
|
}
|
|
|
|
.actions-formulaire {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
.actions-flottantes {
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
padding: 0.6rem 1rem calc(0.6rem + env(safe-area-inset-bottom));
|
|
background: rgba(255, 255, 255, 0.96);
|
|
border-top: 1px solid var(--mb-bord);
|
|
}
|
|
|
|
.actions-flottantes .bouton {
|
|
flex: 1;
|
|
}
|
|
|
|
/* --- Couvertures --- */
|
|
|
|
.couverture {
|
|
position: relative;
|
|
flex: 0 0 auto;
|
|
overflow: hidden;
|
|
border-radius: 6px;
|
|
background: var(--mb-fond-doux);
|
|
border: 1px solid var(--mb-bord);
|
|
}
|
|
|
|
.couverture-petite { width: 3.5rem; height: 5.25rem; }
|
|
.couverture-moyenne { width: 6rem; height: 9rem; }
|
|
|
|
.couverture-substitut {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: #9aa2ad;
|
|
}
|
|
|
|
.couverture-image {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
/* Posée par l'attribut onerror : les couvertures OpenLibrary renvoient
|
|
des 502 intermittents, l'image cassée laisse place au substitut. */
|
|
.couverture-image.image-cassee {
|
|
display: none;
|
|
}
|
|
|
|
/* --- Liste de livres --- */
|
|
|
|
.liste-livres,
|
|
.liste-candidats {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.carte-livre,
|
|
.carte-candidat {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem 0;
|
|
border-bottom: 1px solid var(--mb-bord);
|
|
}
|
|
|
|
.carte-corps {
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
|
|
.carte-titre {
|
|
display: block;
|
|
font-weight: 600;
|
|
color: #1d2129;
|
|
text-decoration: none;
|
|
margin: 0 0 0.15rem;
|
|
}
|
|
|
|
.carte-auteur {
|
|
margin: 0 0 0.25rem;
|
|
color: var(--mb-texte-doux);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.carte-details {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.4rem;
|
|
align-items: center;
|
|
margin: 0 0 0.5rem;
|
|
font-size: 0.8rem;
|
|
color: var(--mb-texte-doux);
|
|
}
|
|
|
|
.etiquette {
|
|
background: var(--mb-fond-doux);
|
|
border-radius: 4px;
|
|
padding: 0.1rem 0.4rem;
|
|
}
|
|
|
|
/* --- Choix entre plusieurs notices --- */
|
|
|
|
/* Les candidats partagent titre et auteur : ce sont l'éditeur et l'année
|
|
qui les départagent, donc ils sont mis en avant visuellement. */
|
|
.candidat-distinction {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
margin: 0 0 0.4rem;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.candidat-editeur {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.candidat-annee {
|
|
background: #eef2f7;
|
|
border-radius: 4px;
|
|
padding: 0.05rem 0.4rem;
|
|
}
|
|
|
|
/* --- Formulaire --- */
|
|
|
|
.formulaire-apercu {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.zone-danger {
|
|
margin-top: 2rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid var(--mb-bord);
|
|
}
|
|
|
|
/* --- PC : simple élargissement, la structure mobile ne change pas. --- */
|
|
|
|
@media (min-width: 48rem) {
|
|
.actions-flottantes {
|
|
position: static;
|
|
padding: 0;
|
|
margin-top: 1.5rem;
|
|
background: none;
|
|
border-top: 0;
|
|
}
|
|
|
|
.actions-flottantes .bouton {
|
|
flex: 0 0 auto;
|
|
}
|
|
}
|