Un code-barre inconnu scanné propose désormais le même formulaire de création que /materiels au lieu d'un simple message d'erreur. La quantité peut être ajustée d'un delta choisi librement (pas figé à ±1) ou définie directement à une valeur absolue après un inventaire. La racine "/" sert directement la liste des matériels sans redirection. La page Matériels gagne un filtre par catégorie, un filtre stock bas, et un tri incluant un regroupement par catégorie.
662 lines
14 KiB
CSS
662 lines
14 KiB
CSS
/*
|
|
* Système de tokens : le langage visuel de l'app s'inspire des étiquettes
|
|
* d'inventaire et du geste du scanner code-barres (la ligne "laser" sous
|
|
* le champ de scan, les rayures de code-barres en motif de rupture).
|
|
*
|
|
* Pas de police externe (Google Fonts, etc.) : le poste peut tourner sur
|
|
* le réseau interne de la clinique, potentiellement sans accès internet.
|
|
* Le "caractère" typographique vient du poids, du tracking et de
|
|
* l'échelle plutôt que d'une police téléchargée.
|
|
*/
|
|
|
|
:root {
|
|
--papier: #f5f3ec; /* fond, blanc "étiquette" chaud */
|
|
--papier-creux: #ebe8de; /* zones en creux (input, table stripe) */
|
|
--encre: #14181c; /* texte principal, quasi noir */
|
|
--acier: #5b6470; /* texte secondaire, bordures */
|
|
--acier-clair: #d8dbdf; /* bordures discrètes */
|
|
--brique: #b8402a; /* alerte stock bas */
|
|
--labo: #2e7d6b; /* stock ok / validation */
|
|
--ambre: #c67a17; /* accent focus / scan actif */
|
|
|
|
--police-corps: -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
|
--police-donnee: ui-monospace, "SF Mono", "Cascadia Code", "Consolas",
|
|
"Liberation Mono", monospace;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: var(--police-corps);
|
|
max-width: 900px;
|
|
margin: 0 auto 4rem;
|
|
padding: 0 1rem;
|
|
color: var(--encre);
|
|
background: var(--papier);
|
|
}
|
|
|
|
h1, h2 {
|
|
font-family: var(--police-corps);
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
/* Focus clavier visible partout, indépendamment du style propre à chaque
|
|
contrôle (accessibilité : ne jamais s'appuyer sur la seule couleur). */
|
|
a:focus-visible,
|
|
button:focus-visible,
|
|
input:focus-visible,
|
|
select:focus-visible {
|
|
outline: 2px solid var(--ambre);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* Motif de rupture "code-barres", réutilisé en petite touche (nav, en-tête
|
|
du scanner, étiquette de résultat) : des barres de largeurs irrégulières
|
|
en dégradé répété, sans image ni police à charger. */
|
|
.rayures-code-barre {
|
|
height: 6px;
|
|
background-image: repeating-linear-gradient(
|
|
90deg,
|
|
var(--encre) 0px, var(--encre) 2px,
|
|
transparent 2px, transparent 5px,
|
|
var(--encre) 5px, var(--encre) 6px,
|
|
transparent 6px, transparent 12px,
|
|
var(--encre) 12px, var(--encre) 15px,
|
|
transparent 15px, transparent 19px
|
|
);
|
|
opacity: 0.85;
|
|
}
|
|
|
|
nav {
|
|
display: flex;
|
|
gap: 1.5rem;
|
|
align-items: center;
|
|
margin: 0 -1rem 2.5rem;
|
|
padding: 1rem 1rem 0.85rem;
|
|
background: var(--encre);
|
|
}
|
|
|
|
nav a {
|
|
text-decoration: none;
|
|
color: var(--papier);
|
|
font-weight: 600;
|
|
font-size: 0.85rem;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
nav a:hover {
|
|
color: var(--ambre);
|
|
}
|
|
|
|
/*
|
|
* Disclosure native (<details>/<summary>) : utilisée partout pour replier
|
|
* par défaut ce qui n'est pas immédiatement utile (branches de
|
|
* l'arborescence, formulaire d'édition d'un matériel). Zéro JavaScript,
|
|
* et ça évite d'afficher d'un coup des centaines de champs de formulaire
|
|
* quand une liste devient longue.
|
|
*/
|
|
details > summary {
|
|
list-style: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
details > summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
|
|
.noeud-chevron {
|
|
flex: none;
|
|
width: 0.55em;
|
|
height: 0.55em;
|
|
border-right: 2px solid var(--acier);
|
|
border-bottom: 2px solid var(--acier);
|
|
transform: rotate(-45deg);
|
|
transition: transform 0.15s ease;
|
|
}
|
|
|
|
details[open] > summary .noeud-chevron {
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
.barre-recherche,
|
|
.barre-filtres {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
.barre-recherche input[type="search"],
|
|
.barre-filtres input[type="search"] {
|
|
flex: 1;
|
|
min-width: 200px;
|
|
max-width: 360px;
|
|
background: var(--papier-creux);
|
|
border: none;
|
|
}
|
|
|
|
.barre-filtres select {
|
|
background: var(--papier-creux);
|
|
border: none;
|
|
}
|
|
|
|
.case-stock-bas {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
font-size: 0.85rem;
|
|
color: var(--acier);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.case-stock-bas input {
|
|
width: auto;
|
|
padding: 0;
|
|
}
|
|
|
|
/* En-tête de groupe quand la liste des matériels est regroupée par
|
|
catégorie (tri=categorie) : un simple séparateur textuel, pas une carte
|
|
à part entière, pour rester léger sur une longue liste. */
|
|
.groupe-categorie-entete {
|
|
font-family: var(--police-donnee);
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.05em;
|
|
text-transform: uppercase;
|
|
color: var(--acier);
|
|
padding: 0.9rem 0.5rem 0.3rem;
|
|
}
|
|
|
|
.lien-effacer {
|
|
font-size: 0.85rem;
|
|
color: var(--acier);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.liste-vide {
|
|
color: var(--acier);
|
|
font-style: italic;
|
|
padding: 1rem 0.25rem;
|
|
}
|
|
|
|
/* Liste plate (résultats de recherche de catégories, destinataires). */
|
|
.liste-resultats {
|
|
list-style: none;
|
|
margin: 0 0 1.5rem;
|
|
padding: 0;
|
|
border-top: 1px solid var(--acier-clair);
|
|
}
|
|
|
|
.liste-resultats li {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 0.6rem 0.25rem;
|
|
border-bottom: 1px solid var(--acier-clair);
|
|
}
|
|
|
|
.chemin-categorie {
|
|
font-family: var(--police-donnee);
|
|
font-size: 0.9rem;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Arborescence de catégories : profondeur illimitée, repliée par défaut.
|
|
------------------------------------------------------------------ */
|
|
|
|
.arbre-categories,
|
|
.arbre-categories ul {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.arbre-categories ul {
|
|
margin-left: 0.9rem;
|
|
padding-left: 0.85rem;
|
|
border-left: 1px solid var(--acier-clair);
|
|
}
|
|
|
|
.noeud-categorie {
|
|
margin: 0.1rem 0;
|
|
}
|
|
|
|
.noeud-categorie > details > summary {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.6rem;
|
|
padding: 0.4rem 0.5rem;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.noeud-categorie > details > summary:hover {
|
|
background: var(--papier-creux);
|
|
}
|
|
|
|
.noeud-nom {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.noeud-compte {
|
|
font-family: var(--police-donnee);
|
|
font-size: 0.75rem;
|
|
color: var(--acier);
|
|
}
|
|
|
|
.noeud-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.75rem;
|
|
align-items: center;
|
|
padding: 0.4rem 0.5rem 0.6rem 1.9rem;
|
|
}
|
|
|
|
.formulaire-racine {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Liste des matériels : lignes compactes en lecture seule, le formulaire
|
|
d'édition complet ne s'affiche qu'une fois la ligne dépliée. C'est ce
|
|
qui évite le débordement horizontal d'un tableau avec un <input> par
|
|
colonne sur chaque ligne, y compris avec une longue liste.
|
|
------------------------------------------------------------------ */
|
|
|
|
.liste-materiels {
|
|
list-style: none;
|
|
margin: 0 0 2rem;
|
|
padding: 0;
|
|
border-top: 1px solid var(--acier-clair);
|
|
}
|
|
|
|
.liste-materiels > li {
|
|
border-bottom: 1px solid var(--acier-clair);
|
|
}
|
|
|
|
.ligne-materiel > summary {
|
|
display: grid;
|
|
grid-template-columns: auto minmax(0, 2fr) minmax(0, 1.3fr) minmax(0, 1fr) auto;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.6rem 0.5rem;
|
|
}
|
|
|
|
.ligne-materiel > summary:hover {
|
|
background: var(--papier-creux);
|
|
}
|
|
|
|
.materiel-nom,
|
|
.materiel-categorie,
|
|
.materiel-code {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.materiel-nom {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.materiel-categorie,
|
|
.materiel-code {
|
|
font-size: 0.8rem;
|
|
color: var(--acier);
|
|
}
|
|
|
|
.materiel-code {
|
|
font-family: var(--police-donnee);
|
|
}
|
|
|
|
.materiel-quantite {
|
|
font-family: var(--police-donnee);
|
|
font-weight: 700;
|
|
text-align: right;
|
|
min-width: 2.5ch;
|
|
}
|
|
|
|
.materiel-quantite.quantite-basse {
|
|
color: var(--brique);
|
|
}
|
|
|
|
.formulaire-edition {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
gap: 0.75rem;
|
|
padding: 0.25rem 0.5rem 1.25rem 1.9rem;
|
|
}
|
|
|
|
.formulaire-edition label {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.3rem;
|
|
font-size: 0.8rem;
|
|
color: var(--acier);
|
|
}
|
|
|
|
.formulaire-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: end;
|
|
}
|
|
|
|
.formulaire-inline {
|
|
display: inline-flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.formulaire-grille {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 0.75rem;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.bouton-danger {
|
|
background: color-mix(in srgb, var(--brique) 12%, white);
|
|
color: var(--brique);
|
|
border: 1px solid color-mix(in srgb, var(--brique) 40%, white);
|
|
}
|
|
|
|
.message-erreur {
|
|
color: var(--brique);
|
|
}
|
|
|
|
input, select, button {
|
|
font-family: inherit;
|
|
font-size: 1rem;
|
|
padding: 0.5rem 0.6rem;
|
|
border: 1px solid var(--acier-clair);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
button {
|
|
cursor: pointer;
|
|
border-color: var(--encre);
|
|
background: white;
|
|
}
|
|
|
|
button:hover {
|
|
background: var(--encre);
|
|
color: var(--papier);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Écran de scan : le seul endroit où on prend un vrai risque visuel.
|
|
Le reste de l'app reste sobre pour ne pas diluer cette signature.
|
|
------------------------------------------------------------------ */
|
|
|
|
.scanner {
|
|
max-width: 480px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.scanner-entete {
|
|
text-align: center;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.scanner-eyebrow {
|
|
font-family: var(--police-donnee);
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.15em;
|
|
text-transform: uppercase;
|
|
color: var(--acier);
|
|
margin: 0 0 0.25rem;
|
|
}
|
|
|
|
.scanner-entete h1 {
|
|
font-size: 1.75rem;
|
|
font-weight: 800;
|
|
margin: 0 0 1rem;
|
|
}
|
|
|
|
.scanner-formulaire {
|
|
position: relative;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.scanner-label {
|
|
display: block;
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--acier);
|
|
margin-bottom: 0.4rem;
|
|
}
|
|
|
|
.scanner-champ {
|
|
width: 100%;
|
|
font-family: var(--police-donnee);
|
|
font-size: 1.5rem;
|
|
letter-spacing: 0.05em;
|
|
padding: 0.9rem 1rem;
|
|
background: var(--papier-creux);
|
|
border: none;
|
|
border-radius: 4px 4px 0 0;
|
|
}
|
|
|
|
.scanner-champ::placeholder {
|
|
color: var(--acier);
|
|
opacity: 0.7;
|
|
}
|
|
|
|
/*
|
|
* La "ligne laser" : un rappel direct du geste physique du scan (le
|
|
* faisceau rouge d'une douchette code-barres). Immobile au repos, elle ne
|
|
* s'anime que quand le champ a le focus, via le sélecteur adjacent
|
|
* input:focus + .laser-ligne (pas besoin de JavaScript).
|
|
*/
|
|
.laser-ligne {
|
|
height: 3px;
|
|
background: var(--acier-clair);
|
|
border-radius: 0 0 4px 4px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.scanner-champ:focus + .laser-ligne {
|
|
background: var(--ambre);
|
|
animation: balayage-laser 1.6s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes balayage-laser {
|
|
0%, 100% { opacity: 0.5; }
|
|
50% { opacity: 1; }
|
|
}
|
|
|
|
/* Code scanné mais inconnu : même carte "étiquette" que le résultat d'un
|
|
scan trouvé, mais avec un formulaire de création à la place du détail
|
|
du stock — pour créer le matériel sans quitter l'écran de scan. */
|
|
.etiquette-creation .etiquette-nom {
|
|
color: var(--brique);
|
|
}
|
|
|
|
.etiquette-sous-titre {
|
|
padding: 0 1.25rem 1.25rem;
|
|
color: var(--acier);
|
|
}
|
|
|
|
.etiquette-formulaire {
|
|
padding: 0 1.25rem 1.25rem;
|
|
max-width: none;
|
|
}
|
|
|
|
/*
|
|
* L'étiquette de résultat : pensée comme une vraie étiquette d'inventaire
|
|
* qu'on viendrait de scanner, avec les rayures de code-barres en tête et
|
|
* une courte animation d'apparition ("tampon" qui vient de s'imprimer).
|
|
*/
|
|
.etiquette-resultat {
|
|
background: white;
|
|
border: 1px solid var(--acier-clair);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
animation: apparition-etiquette 0.25s ease-out;
|
|
}
|
|
|
|
@keyframes apparition-etiquette {
|
|
from { opacity: 0; transform: scale(0.97) translateY(4px); }
|
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
}
|
|
|
|
.etiquette-rayures {
|
|
height: 10px;
|
|
}
|
|
|
|
.etiquette-resultat .etiquette-fil,
|
|
.etiquette-resultat .etiquette-nom,
|
|
.etiquette-resultat .etiquette-quantite,
|
|
.etiquette-resultat .etiquette-statut {
|
|
padding-left: 1.25rem;
|
|
padding-right: 1.25rem;
|
|
}
|
|
|
|
.etiquette-fil {
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
color: var(--acier);
|
|
margin: 1rem 0 0.15rem;
|
|
}
|
|
|
|
.etiquette-nom {
|
|
font-size: 1.3rem;
|
|
font-weight: 800;
|
|
margin: 0 0 1rem;
|
|
}
|
|
|
|
.etiquette-quantite {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.etiquette-nombre {
|
|
font-family: var(--police-donnee);
|
|
font-size: 3rem;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
}
|
|
|
|
.etiquette-unite {
|
|
color: var(--acier);
|
|
}
|
|
|
|
.etiquette-statut {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
font-size: 0.85rem;
|
|
color: var(--acier);
|
|
margin: 0.6rem 0 0;
|
|
}
|
|
|
|
.statut-puce {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
}
|
|
|
|
.statut-ok {
|
|
background: var(--labo);
|
|
}
|
|
|
|
.statut-bas {
|
|
background: var(--brique);
|
|
}
|
|
|
|
/*
|
|
* Deux gestes distincts sur le stock : "ajuster" (formulaire-ajustement,
|
|
* un delta relatif — pas forcément ±1) et "définir" (formulaire-definir,
|
|
* une valeur absolue après un inventaire physique). Volontairement deux
|
|
* formulaires séparés pour ne jamais laisser un doute sur ce qu'un chiffre
|
|
* saisi représente.
|
|
*/
|
|
.etiquette-controles {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
margin-top: 1.25rem;
|
|
padding: 1rem 1.25rem 1.25rem;
|
|
background: var(--papier-creux);
|
|
}
|
|
|
|
.etiquette-champ-quantite {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.3rem;
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
color: var(--acier);
|
|
}
|
|
|
|
.etiquette-champ-quantite input {
|
|
font-family: var(--police-donnee);
|
|
font-size: 1.1rem;
|
|
background: white;
|
|
}
|
|
|
|
.formulaire-definir {
|
|
display: flex;
|
|
align-items: end;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.formulaire-definir .etiquette-champ-quantite {
|
|
flex: 1;
|
|
}
|
|
|
|
.etiquette-actions {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 1px;
|
|
background: var(--acier-clair);
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.bouton-scan {
|
|
width: 100%;
|
|
border: none;
|
|
border-radius: 0;
|
|
padding: 0.9rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.03em;
|
|
background: white;
|
|
}
|
|
|
|
.bouton-scan:hover {
|
|
background: var(--papier-creux);
|
|
color: var(--encre);
|
|
}
|
|
|
|
.bouton-scan:active {
|
|
transform: translateY(1px);
|
|
}
|
|
|
|
.bouton-reception {
|
|
color: var(--labo);
|
|
}
|
|
|
|
.bouton-sortie {
|
|
color: var(--brique);
|
|
}
|
|
|
|
/* Respecte la préférence système : pas d'animation pour qui la désactive. */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.scanner-champ:focus + .laser-ligne {
|
|
animation: none;
|
|
}
|
|
.etiquette-resultat {
|
|
animation: none;
|
|
}
|
|
}
|