Aller au contenu
Bibliotheque UIJABS

Galeries de documents 38 variantes

Tout ce qui sert a presenter, deposer et consulter des fichiers. Les vignettes photo et les apercus de page sont simules en CSS : aucune image n'est chargee, la bibliotheque reste utilisable hors ligne. Le code affiche est celui du fichier css/components/documents.css, tel quel.

A. Grilles de vignettes

Quand l'apparence du fichier compte plus que son nom : photos de chantier, plans, visuels. La vignette doit dire le type ET l'etat en un coup d'oeil.

Facade avant travaux
Combles isolation
Local technique
Groupe exterieur
Grille de photos

Photos de chantier, avant / apres, plans. La grille se resserre toute seule : aucune media query a ecrire.

Voir le codeMasquer le code
<div class="doc-grille">
  <figure class="doc-carte"><div class="doc-vignette doc-photo doc-photo--1"></div><figcaption>Facade avant travaux</figcaption></figure>
  <figure class="doc-carte"><div class="doc-vignette doc-photo doc-photo--2"></div><figcaption>Combles isolation</figcaption></figure>
  <figure class="doc-carte"><div class="doc-vignette doc-photo doc-photo--3"></div><figcaption>Local technique</figcaption></figure>
  <figure class="doc-carte"><div class="doc-vignette doc-photo doc-photo--4"></div><figcaption>Groupe exterieur</figcaption></figure>
</div>
.doc-grille {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--sp-3);
  width: 100%;
}
.doc-carte { margin: 0; display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0; }
.doc-carte figcaption {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--c-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
grid grid-cols-[repeat(auto-fill,minmax(150px,1fr))] gap-3
Grille de documents

Devis, factures, attestations. L'apercu de la premiere page vaut mieux qu'une icone : on reconnait un document a sa mise en page.

Voir le codeMasquer le code
<div class="doc-grille">
  <a href="#" class="doc-carte doc-carte--lien">
    <div class="doc-vignette"><div class="doc-page doc-page--rouge"><div class="doc-page__lignes"></div></div></div>
    <span class="doc-carte__nom">Devis LTP-2026-103.pdf</span>
    <span class="doc-carte__meta">248 Ko - 12 aout</span>
  </a>
  <a href="#" class="doc-carte doc-carte--lien">
    <div class="doc-vignette"><div class="doc-page"><div class="doc-page__lignes"></div></div></div>
    <span class="doc-carte__nom">Attestation TVA 5,5.pdf</span>
    <span class="doc-carte__meta">96 Ko - 12 aout</span>
  </a>
  <a href="#" class="doc-carte doc-carte--lien">
    <div class="doc-vignette"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#1d7044"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">XLS</text></svg></div>
    <span class="doc-carte__nom">Recensement BAR-TH-174.xlsx</span>
    <span class="doc-carte__meta">1,2 Mo - 3 sept.</span>
  </a>
</div>
.doc-carte--lien {
  text-decoration: none;
  color: inherit;
  padding: var(--sp-2);
  margin: calc(var(--sp-2) * -1);
  border-radius: var(--r-md);
  transition: background-color var(--t-fast);
}
.doc-carte--lien:hover { background: var(--c-surface-2); }
.doc-carte--lien:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }
.doc-carte__nom {
  font-size: var(--fs-sm);
  font-weight: var(--fw-semi);
  color: var(--c-text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.doc-carte__meta { font-size: var(--fs-xs); color: var(--c-text-muted); }
group block rounded-lg p-2 hover:bg-slate-50 [&_figcaption]:truncate
Signe
Devis LTP-2026-103
En attente
Mandat MaPrimeRenov
Refuse
Avis d'imposition
Vignette avec statut

Quand l'etat compte autant que le fichier : signe, en attente, refuse. La pastille porte un mot, pas seulement une couleur.

Voir le codeMasquer le code
<div class="doc-grille">
  <figure class="doc-carte">
    <div class="doc-vignette"><div class="doc-page doc-page--rouge"><div class="doc-page__lignes"></div></div>
      <span class="doc-etat doc-etat--ok">Signe</span></div>
    <figcaption>Devis LTP-2026-103</figcaption>
  </figure>
  <figure class="doc-carte">
    <div class="doc-vignette"><div class="doc-page"><div class="doc-page__lignes"></div></div>
      <span class="doc-etat doc-etat--attente">En attente</span></div>
    <figcaption>Mandat MaPrimeRenov</figcaption>
  </figure>
  <figure class="doc-carte">
    <div class="doc-vignette"><div class="doc-page"><div class="doc-page__lignes"></div></div>
      <span class="doc-etat doc-etat--ko">Refuse</span></div>
    <figcaption>Avis d'imposition</figcaption>
  </figure>
</div>
.doc-etat {
  position: absolute;
  top: var(--sp-2);
  left: var(--sp-2);
  padding: 3px 8px;
  border-radius: var(--r-full);
  font-size: 11px;
  font-weight: var(--fw-bold);
  letter-spacing: .01em;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.doc-etat--ok      { background: var(--c-success); color: #fff; }
.doc-etat--attente { background: var(--c-warn);    color: #fff; }
.doc-etat--ko      { background: var(--c-danger);  color: #fff; }
.doc-etat--info    { background: var(--c-info);    color: #fff; }
absolute left-2 top-2 rounded-full px-2 py-0.5 text-[11px] font-bold
Facade avant travaux
Local technique
Grille avec actions au survol

Les actions n'encombrent pas la grille au repos. Sur mobile, ou il n'y a pas de survol, elles restent visibles en permanence.

Voir le codeMasquer le code
<div class="doc-grille">
  <figure class="doc-carte doc-carte--actions">
    <div class="doc-vignette doc-photo doc-photo--1">
      <div class="doc-actions">
        <button class="btn btn--icone btn--verre-sombre btn--sm" aria-label="Agrandir"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5M11 8v6M8 11h6"/></svg></button>
        <button class="btn btn--icone btn--verre-sombre btn--sm" aria-label="Telecharger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
        <button class="btn btn--icone btn--verre-sombre btn--sm" aria-label="Supprimer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg></button>
      </div>
    </div>
    <figcaption>Facade avant travaux</figcaption>
  </figure>
  <figure class="doc-carte doc-carte--actions is-ouvert">
    <div class="doc-vignette doc-photo doc-photo--3">
      <div class="doc-actions">
        <button class="btn btn--icone btn--verre-sombre btn--sm" aria-label="Agrandir"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5M11 8v6M8 11h6"/></svg></button>
        <button class="btn btn--icone btn--verre-sombre btn--sm" aria-label="Telecharger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
        <button class="btn btn--icone btn--verre-sombre btn--sm" aria-label="Supprimer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg></button>
      </div>
    </div>
    <figcaption>Local technique</figcaption>
  </figure>
</div>
.doc-actions {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-2);
  background: linear-gradient(transparent, rgba(0,0,0,.55));
  opacity: 0;
  transform: translateY(6px);
  transition: opacity var(--t-base), transform var(--t-base);
}
.doc-carte--actions:hover .doc-actions,
.doc-carte--actions:focus-within .doc-actions { opacity: 1; transform: translateY(0); }
/* Sans survol (tactile), les actions restent visibles : sinon elles sont perdues. */
@media (hover: none) {
  .doc-actions { opacity: 1; transform: none; }
}
/* is-ouvert force l'affichage, pour une demonstration ou un mode edition. */
.doc-carte--actions.is-ouvert .doc-actions { opacity: 1; transform: none; }
opacity-0 transition group-hover:opacity-100 max-md:opacity-100
Grille selectionnable

Selection multiple pour une action de masse. La case reste affichee des qu'un element est coche, sinon on ne sait plus ou on en est.

Voir le codeMasquer le code
<div class="doc-grille">
  <label class="doc-carte doc-carte--select is-choisi">
    <input type="checkbox" checked>
    <div class="doc-vignette doc-photo doc-photo--2"><span class="doc-case"></span></div>
    <span class="doc-carte__nom">Combles isolation</span>
  </label>
  <label class="doc-carte doc-carte--select">
    <input type="checkbox">
    <div class="doc-vignette doc-photo doc-photo--4"><span class="doc-case"></span></div>
    <span class="doc-carte__nom">Groupe exterieur</span>
  </label>
  <label class="doc-carte doc-carte--select is-choisi">
    <input type="checkbox" checked>
    <div class="doc-vignette doc-photo doc-photo--6"><span class="doc-case"></span></div>
    <span class="doc-carte__nom">Tableau electrique</span>
  </label>
</div>
.doc-carte--select { cursor: pointer; }
.doc-carte--select input { position: absolute; opacity: 0; width: 0; height: 0; }
.doc-carte--select .doc-vignette { transition: box-shadow var(--t-fast); }
.doc-case {
  position: absolute;
  top: var(--sp-2); right: var(--sp-2);
  width: 22px; height: 22px;
  border-radius: var(--r-full);
  border: 2px solid #fff;
  background: rgba(0,0,0,.25);
  box-shadow: 0 1px 4px rgba(0,0,0,.3);
  opacity: 0;
  transition: opacity var(--t-fast), background-color var(--t-fast);
}
.doc-carte--select:hover .doc-case,
.doc-carte--select:has(input:checked) .doc-case { opacity: 1; }
.doc-carte--select:has(input:checked) .doc-case { background: var(--c-primary); }
.doc-carte--select:has(input:checked) .doc-case::after {
  content: "";
  position: absolute;
  top: 4px; left: 6.5px;
  width: 4px; height: 8px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.doc-carte--select:has(input:checked) .doc-vignette { box-shadow: 0 0 0 3px var(--c-primary); }
.doc-carte--select input:focus-visible ~ .doc-vignette { outline: 2px solid var(--c-focus); outline-offset: 3px; }
has-[:checked]:ring-2 has-[:checked]:ring-blue-600
Photos avant travaux 4
Facade
Toiture
Documents administratifs 7
Devis
Grille par sections

Le classement automatique par categorie : chaque section porte son compte et se replie. Evite le mur de 200 vignettes.

Voir le codeMasquer le code
<details class="doc-section" open>
  <summary>Photos avant travaux <span class="doc-section__compte">4</span></summary>
  <div class="doc-grille">
    <figure class="doc-carte"><div class="doc-vignette doc-photo doc-photo--1"></div><figcaption>Facade</figcaption></figure>
    <figure class="doc-carte"><div class="doc-vignette doc-photo doc-photo--6"></div><figcaption>Toiture</figcaption></figure>
  </div>
</details>
<details class="doc-section">
  <summary>Documents administratifs <span class="doc-section__compte">7</span></summary>
  <div class="doc-grille">
    <figure class="doc-carte"><div class="doc-vignette"><div class="doc-page doc-page--rouge"><div class="doc-page__lignes"></div></div></div><figcaption>Devis</figcaption></figure>
  </div>
</details>
.doc-section { width: 100%; border-bottom: 1px solid var(--c-border); }
.doc-section > summary {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3) 0;
  font-size: var(--fs-md);
  font-weight: var(--fw-semi);
  color: var(--c-text);
  cursor: pointer;
  list-style: none;
}
.doc-section > summary::-webkit-details-marker { display: none; }
.doc-section > summary::before {
  content: "";
  width: 7px; height: 7px;
  border-right: 2px solid var(--c-text-muted);
  border-bottom: 2px solid var(--c-text-muted);
  transform: rotate(-45deg);
  transition: transform var(--t-base);
}
.doc-section[open] > summary::before { transform: rotate(45deg); }
.doc-section__compte {
  min-width: 20px;
  padding: 2px 7px;
  border-radius: var(--r-full);
  background: var(--c-surface-3);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  color: var(--c-text-soft);
}
.doc-section > .doc-grille { padding-bottom: var(--sp-4); }
[&>summary]:flex [&>summary]:cursor-pointer [&>summary]:items-center [&>summary]:gap-2 [&>summary]:py-2 [&>summary]:font-semibold
Mosaique irreguliere

Galerie de realisations d'un site vitrine, ou les proportions varient. Le premier element occupe deux colonnes.

Voir le codeMasquer le code
<div class="doc-mosaique">
  <figure class="doc-carte doc-carte--large"><div class="doc-vignette doc-photo doc-photo--1"></div></figure>
  <figure class="doc-carte"><div class="doc-vignette doc-photo doc-photo--2"></div></figure>
  <figure class="doc-carte"><div class="doc-vignette doc-photo doc-photo--3"></div></figure>
  <figure class="doc-carte"><div class="doc-vignette doc-photo doc-photo--4"></div></figure>
  <figure class="doc-carte doc-carte--haute"><div class="doc-vignette doc-photo doc-photo--5"></div></figure>
</div>
.doc-mosaique {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 110px;
  gap: var(--sp-2);
  width: 100%;
}
.doc-mosaique .doc-carte { height: 100%; }
.doc-mosaique .doc-vignette { aspect-ratio: auto; height: 100%; }
.doc-carte--large { grid-column: span 2; grid-row: span 2; }
.doc-carte--haute { grid-row: span 2; }
@media (max-width: 560px) {
  .doc-mosaique { grid-template-columns: repeat(2, 1fr); }
  .doc-carte--large { grid-column: span 2; grid-row: span 1; }
}
grid grid-cols-4 gap-2 [&>*:first-child]:col-span-2 [&>*:first-child]:row-span-2

B. Listes et tableaux

Quand le nom, la date et l'auteur comptent plus que l'apparence. C'est le mode par defaut d'un CRM : plus dense, triable, lisible sur mobile.

Liste simple

Pieces jointes d'un devis, d'un courriel, d'une fiche. Le type de fichier est porte par l'icone, pas par l'extension du nom.

Voir le codeMasquer le code
<ul class="doc-liste">
  <li><a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#e8453c"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">PDF</text></svg><span class="doc-liste__nom">Devis LTP-2026-103.pdf</span><span class="doc-liste__taille">248 Ko</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></a></li>
  <li><a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#7c3aed"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">JPG</text></svg><span class="doc-liste__nom">Photo avant travaux.jpg</span><span class="doc-liste__taille">1,8 Mo</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></a></li>
  <li><a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#1d7044"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">XLS</text></svg><span class="doc-liste__nom">Recensement BAR-TH-174.xlsx</span><span class="doc-liste__taille">1,2 Mo</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></a></li>
</ul>
.doc-liste { list-style: none; margin: 0; padding: 0; width: 100%; border: 1px solid var(--c-border); border-radius: var(--r-md); overflow: hidden; background: var(--c-surface); }
.doc-liste > li + li { border-top: 1px solid var(--c-border); }
.doc-liste a, .doc-liste > li > .doc-liste__ligne {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3);
  text-decoration: none;
  color: var(--c-text);
  transition: background-color var(--t-fast);
}
.doc-liste a:hover { background: var(--c-surface-2); }
.doc-liste a:focus-visible { outline: 2px solid var(--c-focus); outline-offset: -2px; }
.doc-liste svg { width: 24px; height: 24px; flex: none; }
.doc-liste a > svg:last-child { width: 18px; height: 18px; color: var(--c-text-muted); }
.doc-liste__nom { flex: 1; min-width: 0; font-size: var(--fs-md); font-weight: var(--fw-medium); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.doc-liste__taille { font-size: var(--fs-xs); color: var(--c-text-muted); white-space: nowrap; }
divide-y rounded-lg border [&_a]:flex [&_a]:items-center [&_a]:gap-3 [&_a]:p-3 hover:[&_a]:bg-slate-50
Liste detaillee

Le mode par defaut d'un CRM. Deux lignes par fichier : le nom, puis les metadonnees qui servent a le retrouver.

Voir le codeMasquer le code
<ul class="doc-liste doc-liste--detail">
  <li><a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#e8453c"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">PDF</text></svg>
    <span class="doc-liste__bloc"><strong>Devis LTP-2026-103.pdf</strong><small>Ajoute par Julien K. - 12 aout 2026 - 248 Ko</small></span>
    <span class="doc-etiq doc-etiq--ok">Signe</span>
    <button class="btn btn--icone btn--fantome btn--sm" aria-label="Actions"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button>
  </a></li>
  <li><a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#2b579a"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">DOC</text></svg>
    <span class="doc-liste__bloc"><strong>Mandat MaPrimeRenov.docx</strong><small>Ajoute par Sophie M. - 9 aout 2026 - 84 Ko</small></span>
    <span class="doc-etiq doc-etiq--attente">A signer</span>
    <button class="btn btn--icone btn--fantome btn--sm" aria-label="Actions"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button>
  </a></li>
</ul>
.doc-liste__bloc { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.doc-liste__bloc strong { font-size: var(--fs-md); font-weight: var(--fw-semi); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.doc-liste__bloc small  { font-size: var(--fs-xs); color: var(--c-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.doc-etiq {
  padding: 3px 9px;
  border-radius: var(--r-full);
  font-size: 11px;
  font-weight: var(--fw-bold);
  white-space: nowrap;
}
.doc-etiq--ok      { background: var(--c-success-soft); color: var(--c-success); }
.doc-etiq--attente { background: var(--c-warn-soft);    color: var(--c-warn); }
.doc-etiq--ko      { background: var(--c-danger-soft);  color: var(--c-danger); }
.doc-etiq--neutre  { background: var(--c-surface-3);    color: var(--c-text-soft); }
[&_strong]:block [&_small]:text-xs [&_small]:text-slate-500
DocumentTypeAjoute leTaille
Devis LTP-2026-103.pdf Devis12/08/2026248 Ko
Photo toiture.jpg Photo09/08/20261,8 Mo
Tableau de documents

Volume important, tri par colonne, selection multiple. Sur mobile le tableau bascule en cartes plutot que de defiler.

Voir le codeMasquer le code
<table class="doc-table">
  <thead><tr>
    <th class="doc-table__case"><label class="coche"><input type="checkbox"><span class="coche__boite"></span></label></th>
    <th>Document</th><th>Type</th><th>Ajoute le</th><th>Taille</th><th></th>
  </tr></thead>
  <tbody>
    <tr>
      <td class="doc-table__case"><label class="coche"><input type="checkbox" checked><span class="coche__boite"></span></label></td>
      <td data-col="Document"><span class="doc-table__nom"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#e8453c"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">PDF</text></svg> Devis LTP-2026-103.pdf</span></td>
      <td data-col="Type">Devis</td><td data-col="Ajoute le">12/08/2026</td><td data-col="Taille">248 Ko</td>
      <td><button class="btn btn--icone btn--fantome btn--sm" aria-label="Telecharger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button></td>
    </tr>
    <tr>
      <td class="doc-table__case"><label class="coche"><input type="checkbox"><span class="coche__boite"></span></label></td>
      <td data-col="Document"><span class="doc-table__nom"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#7c3aed"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">JPG</text></svg> Photo toiture.jpg</span></td>
      <td data-col="Type">Photo</td><td data-col="Ajoute le">09/08/2026</td><td data-col="Taille">1,8 Mo</td>
      <td><button class="btn btn--icone btn--fantome btn--sm" aria-label="Telecharger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button></td>
    </tr>
  </tbody>
</table>
.doc-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--r-md); overflow: hidden; }
.doc-table th {
  padding: var(--sp-2) var(--sp-3);
  text-align: left;
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--c-text-muted);
  background: var(--c-surface-2);
  border-bottom: 1px solid var(--c-border);
  white-space: nowrap;
}
.doc-table td { padding: var(--sp-2) var(--sp-3); color: var(--c-text); vertical-align: middle; }
.doc-table tbody tr + tr td { border-top: 1px solid var(--c-border); }
.doc-table tbody tr:hover { background: var(--c-surface-2); }
.doc-table tbody tr:has(input:checked) { background: var(--c-primary-soft); }
.doc-table__case { width: 40px; }
.doc-table__nom { display: inline-flex; align-items: center; gap: var(--sp-2); font-weight: var(--fw-medium); }
.doc-table__nom svg { width: 20px; height: 20px; flex: none; }
/* Sous 640 px le tableau devient une pile de cartes : chaque cellule affiche
   son intitule de colonne. Aucun defilement horizontal. */
@media (max-width: 640px) {
  .doc-table thead { display: none; }
  .doc-table, .doc-table tbody, .doc-table tr, .doc-table td { display: block; width: 100%; }
  .doc-table tr { border: 1px solid var(--c-border); border-radius: var(--r-md); margin-bottom: var(--sp-2); padding: var(--sp-2); }
  .doc-table tbody tr + tr td { border-top: none; }
  .doc-table td { display: flex; justify-content: space-between; gap: var(--sp-3); padding: 4px 0; }
  .doc-table td::before { content: attr(data-col); font-size: var(--fs-xs); color: var(--c-text-muted); font-weight: var(--fw-semi); }
  .doc-table td:not([data-col])::before { content: none; }
}
w-full text-sm [&_th]:px-3 [&_th]:py-2 [&_th]:text-left [&_td]:px-3 [&_td]:py-2 [&_tbody_tr]:border-t
Liste groupee par date

Flux d'ajouts recents, journal d'un dossier. Les intitules restent colles en haut pendant le defilement.

Voir le codeMasquer le code
<div class="doc-journal">
  <h4>Aujourd'hui</h4>
  <ul class="doc-liste">
    <li><a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#e8453c"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">PDF</text></svg><span class="doc-liste__nom">Facture LTP-2026-118.pdf</span><span class="doc-liste__taille">14:32</span></a></li>
    <li><a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#7c3aed"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">JPG</text></svg><span class="doc-liste__nom">Photo apres pose.jpg</span><span class="doc-liste__taille">11:05</span></a></li>
  </ul>
  <h4>Hier</h4>
  <ul class="doc-liste">
    <li><a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#2b579a"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">DOC</text></svg><span class="doc-liste__nom">Compte rendu visite.docx</span><span class="doc-liste__taille">17:48</span></a></li>
  </ul>
</div>
.doc-journal { width: 100%; display: flex; flex-direction: column; gap: var(--sp-2); }
.doc-journal h4 {
  position: sticky;
  top: 0;
  z-index: 2;
  margin: var(--sp-3) 0 0;
  padding: var(--sp-2) 0;
  background: var(--c-surface);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--c-text-muted);
}
.doc-journal h4:first-child { margin-top: 0; }
[&>h4]:sticky [&>h4]:top-0 [&>h4]:bg-white [&>h4]:py-2 [&>h4]:text-xs [&>h4]:uppercase
Dossier 2026-103 12
Administratif 5 Devis signe.pdf Attestation TVA.pdf
Photos chantier 7 Avant travaux.jpg
Recensement.xlsx
Arborescence

Navigation dans des dossiers imbriques. Les <details> natifs suffisent : pas une ligne de JavaScript, et le clavier fonctionne d'office.

Voir le codeMasquer le code
<div class="doc-arbre">
  <details open>
    <summary><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M3 6a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg> Dossier 2026-103 <span class="doc-section__compte">12</span></summary>
    <details open>
      <summary><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M3 6a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg> Administratif <span class="doc-section__compte">5</span></summary>
      <a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#e8453c"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">PDF</text></svg> Devis signe.pdf</a>
      <a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#e8453c"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">PDF</text></svg> Attestation TVA.pdf</a>
    </details>
    <details>
      <summary><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M3 6a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg> Photos chantier <span class="doc-section__compte">7</span></summary>
      <a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#7c3aed"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">JPG</text></svg> Avant travaux.jpg</a>
    </details>
    <a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#1d7044"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">XLS</text></svg> Recensement.xlsx</a>
  </details>
</div>
.doc-arbre { width: 100%; font-size: var(--fs-sm); }
.doc-arbre details { padding-left: var(--sp-4); }
.doc-arbre > details { padding-left: 0; }
.doc-arbre summary {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 5px var(--sp-2);
  border-radius: var(--r-sm);
  font-weight: var(--fw-semi);
  color: var(--c-text);
  cursor: pointer;
  list-style: none;
}
.doc-arbre summary::-webkit-details-marker { display: none; }
.doc-arbre summary:hover { background: var(--c-surface-2); }
.doc-arbre summary svg { width: 17px; height: 17px; color: var(--c-warn); flex: none; }
.doc-arbre a {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 5px var(--sp-2) 5px var(--sp-6);
  border-radius: var(--r-sm);
  color: var(--c-text-soft);
  text-decoration: none;
}
.doc-arbre a:hover { background: var(--c-surface-2); color: var(--c-text); }
.doc-arbre a svg { width: 17px; height: 17px; flex: none; }
[&_details]:pl-4 [&_summary]:cursor-pointer [&_a]:flex [&_a]:gap-2 [&_a]:py-1
Fil d'Ariane de dossiers

Se situer dans l'arborescence et remonter d'un cran. Le dernier element n'est pas un lien : c'est la position courante.

Voir le codeMasquer le code
<nav class="doc-fil" aria-label="Chemin">
  <a href="#"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M3 6a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg> Documents</a><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>
  <a href="#">Dossier 2026-103</a><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>
  <a href="#">Photos chantier</a><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>
  <span aria-current="page">Avant travaux</span>
</nav>
.doc-fil { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); font-size: var(--fs-sm); }
.doc-fil a {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px var(--sp-2);
  border-radius: var(--r-sm);
  color: var(--c-text-soft);
  text-decoration: none;
}
.doc-fil a:hover { background: var(--c-surface-2); color: var(--c-text); }
.doc-fil > svg { width: 14px; height: 14px; color: var(--c-text-muted); flex: none; }
.doc-fil a svg { width: 15px; height: 15px; color: var(--c-warn); }
.doc-fil [aria-current] { padding: 3px var(--sp-2); font-weight: var(--fw-semi); color: var(--c-text); }
flex flex-wrap items-center gap-1 text-sm [&_svg]:h-3.5 [&_svg]:text-slate-400
12 documents
Bascule grille / liste

Le meme jeu de fichiers, deux presentations. Le choix se retient d'une visite a l'autre, sinon l'utilisateur le refait a chaque fois.

Voir le codeMasquer le code
<div class="doc-barre-vue">
  <span class="doc-barre-vue__compte">12 documents</span>
  <div class="btn-segmente">
    <button class="btn is-actif" aria-label="Vue grille"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></button>
    <button class="btn" aria-label="Vue liste"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg></button>
  </div>
</div>
.doc-barre-vue {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  width: 100%;
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--c-border);
}
.doc-barre-vue__compte { font-size: var(--fs-sm); font-weight: var(--fw-semi); color: var(--c-text-soft); }
.doc-barre-vue .btn-segmente > .btn { padding: 0 var(--sp-3); }
.doc-barre-vue .btn-segmente svg { width: 17px; height: 17px; }
flex items-center justify-between gap-3 border-b pb-3

C. Depot et televersement

Trois etats obligatoires par fichier : en attente, en cours, termine, plus l'erreur. Une zone de depot porte toujours une porte de sortie cliquable : tout le monde ne glisse pas des fichiers.

Zone de depot

La forme complete : icone, phrase d'action, contraintes, et une porte de sortie cliquable. Tout le monde ne glisse pas des fichiers.

Voir le codeMasquer le code
<label class="doc-depot">
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
  <strong>Glissez vos documents ici</strong>
  <span>ou <u>parcourez vos fichiers</u></span>
  <small>PDF, JPG, PNG - 10 Mo par fichier - 20 fichiers maximum</small>
  <input type="file" multiple hidden>
</label>
.doc-depot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: var(--sp-10) var(--sp-6);
  border: 2px dashed var(--c-border-strong);
  border-radius: var(--r-lg);
  background: var(--c-surface-2);
  text-align: center;
  cursor: pointer;
  transition: border-color var(--t-fast), background-color var(--t-fast);
}
.doc-depot svg { width: 34px; height: 34px; color: var(--c-text-muted); margin-bottom: var(--sp-1); }
.doc-depot strong { font-size: var(--fs-lg); font-weight: var(--fw-semi); color: var(--c-text); }
.doc-depot span   { font-size: var(--fs-md); color: var(--c-text-soft); }
.doc-depot u      { color: var(--c-primary-ink); text-underline-offset: 3px; }
.doc-depot small  { font-size: var(--fs-xs); color: var(--c-text-muted); margin-top: var(--sp-2); }
.doc-depot:hover, .doc-depot.is-survol {
  border-color: var(--c-primary);
  background: var(--c-primary-soft);
}
.doc-depot:hover svg, .doc-depot.is-survol svg { color: var(--c-primary); }
.doc-depot:has(input:focus-visible) { outline: 2px solid var(--c-focus); outline-offset: 3px; }
flex w-full flex-col items-center gap-2 rounded-xl border-2 border-dashed p-10 hover:border-blue-500 hover:bg-blue-50/50
Depot compact

Quand la zone pleine hauteur prendrait trop de place : formulaire long, panneau lateral, ligne de tableau.

Voir le codeMasquer le code
<label class="doc-depot doc-depot--compact">
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg> <strong>Ajouter un fichier</strong> <span>ou glissez-le ici</span>
  <input type="file" hidden>
</label>
.doc-depot--compact {
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-4);
  border-width: 1.5px;
}
.doc-depot--compact svg { width: 20px; height: 20px; margin: 0; }
.doc-depot--compact strong { font-size: var(--fs-md); }
.doc-depot--compact span { font-size: var(--fs-sm); color: var(--c-text-muted); }
flex-row items-center justify-center gap-2 p-4
  • Devis signe.pdf248 Ko - envoye
  • Photo toiture.jpg1,8 Mo - 64 %
  • Photo facade.jpg2,1 Mo - en attente
  • Archive photos.zipFichier trop volumineux (42 Mo)
File d'envoi

Les quatre etats reunis. Chaque ligne garde son bouton d'annulation ou de reprise : un envoi qui echoue sans recours fait tout recommencer.

Voir le codeMasquer le code
<ul class="doc-file">
  <li class="doc-file__ligne is-fait">
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#e8453c"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">PDF</text></svg>
    <span class="doc-liste__bloc"><strong>Devis signe.pdf</strong><small>248 Ko - envoye</small></span>
    <span class="doc-file__ok"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg></span>
    <button class="btn btn--icone btn--fantome btn--sm" aria-label="Retirer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </li>
  <li class="doc-file__ligne is-encours" style="--p: 64%">
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#7c3aed"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">JPG</text></svg>
    <span class="doc-liste__bloc"><strong>Photo toiture.jpg</strong><small>1,8 Mo - 64 %</small><span class="doc-file__barre"></span></span>
    <button class="btn btn--icone btn--fantome btn--sm" aria-label="Annuler l'envoi"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </li>
  <li class="doc-file__ligne is-attente">
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#7c3aed"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">JPG</text></svg>
    <span class="doc-liste__bloc"><strong>Photo facade.jpg</strong><small>2,1 Mo - en attente</small></span>
    <button class="btn btn--icone btn--fantome btn--sm" aria-label="Retirer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </li>
  <li class="doc-file__ligne is-erreur">
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#b45309"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">ZIP</text></svg>
    <span class="doc-liste__bloc"><strong>Archive photos.zip</strong><small>Fichier trop volumineux (42 Mo)</small></span>
    <button class="btn btn--secondaire btn--dense">Reessayer</button>
    <button class="btn btn--icone btn--fantome btn--sm" aria-label="Retirer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </li>
</ul>
.doc-file { list-style: none; margin: 0; padding: 0; width: 100%; display: flex; flex-direction: column; gap: var(--sp-2); }
.doc-file__ligne {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-surface);
}
.doc-file__ligne > svg:first-child { width: 24px; height: 24px; flex: none; }
.doc-file__barre {
  display: block;
  height: 4px;
  margin-top: 6px;
  border-radius: var(--r-full);
  background: var(--c-surface-3);
  overflow: hidden;
}
.doc-file__barre::after {
  content: "";
  display: block;
  height: 100%;
  width: var(--p, 0%);
  border-radius: inherit;
  background: var(--c-primary);
  transition: width var(--t-slow);
}
.doc-file__ok { display: inline-flex; color: var(--c-success); }
.doc-file__ok svg { width: 20px; height: 20px; }
.doc-file__ligne.is-attente { opacity: .62; }
.doc-file__ligne.is-erreur  { border-color: var(--c-danger); background: var(--c-danger-soft); }
.doc-file__ligne.is-erreur small { color: var(--c-danger); font-weight: var(--fw-semi); }
.doc-file__ligne.is-fait    { border-color: var(--c-success); }
[&_li]:flex [&_li]:items-center [&_li]:gap-3 [&_li]:rounded-lg [&_li]:border [&_li]:p-3
Envoi de 3 fichiers2 sur 3 - 4,2 Mo sur 6,1 Mo
Progression globale

Au-dessus de la file : un seul chiffre qui repond a "c'est bientot fini ?". Sans lui, l'utilisateur quitte la page en plein envoi.

Voir le codeMasquer le code
<div class="doc-progres">
  <div class="doc-progres__tete"><strong>Envoi de 3 fichiers</strong><span>2 sur 3 - 4,2 Mo sur 6,1 Mo</span></div>
  <div class="doc-progres__barre" style="--p: 68%"></div>
  <button class="btn btn--secondaire btn--dense">Tout annuler</button>
</div>
.doc-progres {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  width: 100%;
  padding: var(--sp-4);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-surface);
}
.doc-progres__tete { display: flex; flex-direction: column; gap: 2px; min-width: 130px; flex: none; }
.doc-progres__tete strong { font-size: var(--fs-md); font-weight: var(--fw-semi); }
.doc-progres__tete span   { font-size: var(--fs-xs); color: var(--c-text-muted); }
.doc-progres__barre {
  flex: 1 1 90px;
  min-width: 70px;
  height: 8px;
  border-radius: var(--r-full);
  background: var(--c-surface-3);
  overflow: hidden;
}
.doc-progres__barre::after {
  content: "";
  display: block;
  height: 100%;
  width: var(--p, 0%);
  border-radius: inherit;
  background: linear-gradient(90deg, var(--c-primary), var(--c-accent));
  transition: width var(--t-slow);
}
@media (max-width: 560px) { .doc-progres { flex-wrap: wrap; } .doc-progres__barre { order: 3; width: 100%; flex: none; } }
flex items-center gap-4 rounded-lg border bg-white p-4
IMG_4471.jpg 72 % - 1,4 Mo
IMG_4472.jpg Pret a envoyer
Apercu avant envoi

Confirmer qu'on a pris le bon fichier avant de l'envoyer. La vignette leve l'ambiguite entre IMG_4471 et IMG_4472.

Voir le codeMasquer le code
<div class="doc-grille">
  <figure class="doc-carte doc-carte--apercu">
    <div class="doc-vignette doc-photo doc-photo--2">
      <button class="btn btn--icone btn--sm doc-carte__retirer" aria-label="Retirer ce fichier"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
      <span class="doc-anneau" style="--p: 72"></span>
    </div>
    <span class="doc-carte__nom">IMG_4471.jpg</span>
    <span class="doc-carte__meta">72 % - 1,4 Mo</span>
  </figure>
  <figure class="doc-carte doc-carte--apercu">
    <div class="doc-vignette doc-photo doc-photo--5">
      <button class="btn btn--icone btn--sm doc-carte__retirer" aria-label="Retirer ce fichier"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
    </div>
    <span class="doc-carte__nom">IMG_4472.jpg</span>
    <span class="doc-carte__meta">Pret a envoyer</span>
  </figure>
</div>
.doc-carte__retirer {
  position: absolute;
  top: 6px; right: 6px;
  width: 24px; height: 24px;
  min-width: 24px;
  padding: 0;
  border-radius: var(--r-full);
  background: rgba(15,18,24,.62);
  color: #fff;
  border: none;
  z-index: 2;
}
.doc-carte__retirer:hover { background: var(--c-danger); }
.doc-carte__retirer svg { width: 13px; height: 13px; }
/* Anneau de progression pose sur la vignette : lisible meme sur photo claire. */
.doc-anneau {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 46px; height: 46px;
  border-radius: 50%;
  background:
    conic-gradient(#fff calc(var(--p) * 1%), rgba(255,255,255,.28) 0),
    rgba(15,18,24,.45);
  -webkit-mask: radial-gradient(circle, transparent 13px, #000 14px);
          mask: radial-gradient(circle, transparent 13px, #000 14px);
}
relative + conic-gradient pour l'anneau de progression

D. Pieces attendues

Le cas d'un dossier a constituer : la liste des pieces est connue d'avance, chacune porte un etat. C'est la forme utilisee dans les CRM d'artisan et les dossiers d'aide.

  • Avis d'imposition 2025Recu le 09/08 - valide par Sophie M.
  • Devis signeRecu le 12/08 - en cours de verification
  • Justificatif de domicileRefuse : document de plus de 3 mois
  • RIB du beneficiaireObligatoire pour le versement
Liste des pieces attendues

Le coeur d'un dossier d'aide ou de financement. Quatre etats : manquante, recue, validee, refusee. L'icone et le mot portent l'etat, jamais la couleur seule.

Voir le codeMasquer le code
<ul class="doc-pieces">
  <li class="doc-piece is-validee">
    <span class="doc-piece__puce"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg></span>
    <span class="doc-liste__bloc"><strong>Avis d'imposition 2025</strong><small>Recu le 09/08 - valide par Sophie M.</small></span>
    <button class="btn btn--fantome btn--dense"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg> Voir</button>
  </li>
  <li class="doc-piece is-recue">
    <span class="doc-piece__puce"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg></span>
    <span class="doc-liste__bloc"><strong>Devis signe</strong><small>Recu le 12/08 - en cours de verification</small></span>
    <button class="btn btn--fantome btn--dense"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg> Voir</button>
  </li>
  <li class="doc-piece is-refusee">
    <span class="doc-piece__puce"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg></span>
    <span class="doc-liste__bloc"><strong>Justificatif de domicile</strong><small>Refuse : document de plus de 3 mois</small></span>
    <button class="btn btn--secondaire btn--dense">Redeposer</button>
  </li>
  <li class="doc-piece is-manquante">
    <span class="doc-piece__puce"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></span>
    <span class="doc-liste__bloc"><strong>RIB du beneficiaire</strong><small>Obligatoire pour le versement</small></span>
    <button class="btn btn--plein btn--dense">Deposer</button>
  </li>
</ul>
.doc-pieces { list-style: none; margin: 0; padding: 0; width: 100%; display: flex; flex-direction: column; gap: var(--sp-2); }
.doc-piece {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3);
  border: 1px solid var(--c-border);
  border-left-width: 4px;
  border-radius: var(--r-md);
  background: var(--c-surface);
}
.doc-piece__puce {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px; height: 30px;
  flex: none;
  border-radius: var(--r-full);
  background: var(--c-surface-3);
  color: var(--c-text-muted);
}
.doc-piece__puce svg { width: 16px; height: 16px; }
.doc-piece.is-validee   { border-left-color: var(--c-success); }
.doc-piece.is-validee   .doc-piece__puce { background: var(--c-success-soft); color: var(--c-success); }
.doc-piece.is-recue     { border-left-color: var(--c-info); }
.doc-piece.is-recue     .doc-piece__puce { background: var(--c-info-soft); color: var(--c-info); }
.doc-piece.is-refusee   { border-left-color: var(--c-danger); background: var(--c-danger-soft); }
.doc-piece.is-refusee   .doc-piece__puce { background: var(--c-danger); color: #fff; }
.doc-piece.is-refusee   small { color: var(--c-danger); font-weight: var(--fw-semi); }
.doc-piece.is-manquante { border-left-color: var(--c-border-strong); border-style: dashed; }
[&_li]:flex [&_li]:items-center [&_li]:gap-3 [&_li]:border-l-4 [&_li]:p-3
Cases de pieces en grille

La meme liste en grille, chaque case portant son propre depot. Adapte quand les pieces sont nombreuses et de meme importance.

Voir le codeMasquer le code
<div class="doc-cases">
  <label class="doc-case-piece is-remplie">
    <div class="doc-case-piece__apercu"><div class="doc-page doc-page--rouge"><div class="doc-page__lignes"></div></div></div>
    <strong>Devis signe</strong><small>248 Ko - PDF</small>
  </label>
  <label class="doc-case-piece is-remplie">
    <div class="doc-case-piece__apercu doc-photo doc-photo--6"></div>
    <strong>Photo compteur</strong><small>1,1 Mo - JPG</small>
  </label>
  <label class="doc-case-piece">
    <div class="doc-case-piece__apercu"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></div>
    <strong>Avis d'imposition</strong><small>Obligatoire</small>
    <input type="file" hidden>
  </label>
  <label class="doc-case-piece">
    <div class="doc-case-piece__apercu"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></div>
    <strong>RIB</strong><small>Obligatoire</small>
    <input type="file" hidden>
  </label>
</div>
.doc-cases {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--sp-3);
  width: 100%;
}
.doc-case-piece {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: var(--sp-3);
  border: 2px dashed var(--c-border-strong);
  border-radius: var(--r-lg);
  background: var(--c-surface-2);
  cursor: pointer;
  transition: border-color var(--t-fast), background-color var(--t-fast);
}
.doc-case-piece__apercu {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  margin-bottom: var(--sp-1);
  border-radius: var(--r-sm);
  /* background-color et non background : le raccourci effacerait le
     background-image pose par .doc-photo--* sur le meme element. */
  background-color: var(--c-surface-3);
  background-size: cover;
  background-position: center;
  overflow: hidden;
}
.doc-case-piece__apercu svg { width: 26px; height: 26px; color: var(--c-text-muted); }
.doc-case-piece strong { font-size: var(--fs-sm); font-weight: var(--fw-semi); color: var(--c-text); }
.doc-case-piece small  { font-size: var(--fs-xs); color: var(--c-text-muted); }
.doc-case-piece:hover { border-color: var(--c-primary); background: var(--c-primary-soft); }
.doc-case-piece.is-remplie {
  border-style: solid;
  border-color: var(--c-success);
  background: var(--c-surface);
}
grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-3 [&_label]:rounded-xl [&_label]:border-2 [&_label]:border-dashed
Dossier 2026-103 2 pieces manquantes
Devis Avis d'imposition Photo compteur RIB Justificatif de domicile
Avancement du dossier

En tete de la liste des pieces : combien il en manque, et lesquelles. Un pourcentage seul ne dit pas quoi faire ensuite.

Voir le codeMasquer le code
<div class="doc-avancement">
  <div class="doc-avancement__tete">
    <strong>Dossier 2026-103</strong>
    <span class="doc-etiq doc-etiq--attente">2 pieces manquantes</span>
  </div>
  <div class="doc-avancement__jauge"><span style="--p: 66%"></span></div>
  <div class="doc-avancement__pas">
    <span class="is-fait"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg> Devis</span>
    <span class="is-fait"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg> Avis d'imposition</span>
    <span class="is-fait"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg> Photo compteur</span>
    <span>RIB</span>
    <span>Justificatif de domicile</span>
  </div>
</div>
.doc-avancement {
  width: 100%;
  padding: var(--sp-4);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
}
.doc-avancement__tete { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-3); }
.doc-avancement__tete strong { font-size: var(--fs-lg); font-weight: var(--fw-bold); }
.doc-avancement__jauge { height: 8px; border-radius: var(--r-full); background: var(--c-surface-3); overflow: hidden; }
.doc-avancement__jauge span {
  display: block;
  height: 100%;
  width: var(--p, 0%);
  border-radius: inherit;
  background: var(--c-success);
  transition: width var(--t-slow);
}
.doc-avancement__pas { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-3); }
.doc-avancement__pas span {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 10px;
  border-radius: var(--r-full);
  border: 1px dashed var(--c-border-strong);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  color: var(--c-text-muted);
}
.doc-avancement__pas span.is-fait {
  border-style: solid;
  border-color: transparent;
  background: var(--c-success-soft);
  color: var(--c-success);
  font-weight: var(--fw-semi);
}
.doc-avancement__pas svg { width: 13px; height: 13px; }
rounded-xl border bg-white p-4 [&_span.is-fait]:bg-green-50 [&_span.is-fait]:text-green-700

E. Consultation

Visionneuse plein ecran, panneau de detail, apercu PDF, comparaison avant / apres.

Facade avant travaux.jpg 3 sur 12
Visionneuse plein ecran

Ouverture d'une photo depuis la grille. Fermeture par la croix, par Echap et par un clic dans le vide : les trois, toujours.

Voir le codeMasquer le code
<div class="doc-visio">
  <div class="doc-visio__barre">
    <span class="doc-visio__titre">Facade avant travaux.jpg <small>3 sur 12</small></span>
    <span class="doc-visio__outils">
      <button class="btn btn--icone btn--verre-sombre btn--sm" aria-label="Pivoter"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7M21 3v6h-6"/></svg></button>
      <button class="btn btn--icone btn--verre-sombre btn--sm" aria-label="Telecharger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
      <button class="btn btn--icone btn--verre-sombre btn--sm" aria-label="Fermer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
    </span>
  </div>
  <button class="btn btn--icone btn--rond btn--verre-sombre doc-visio__nav doc-visio__nav--g" aria-label="Precedent"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg></button>
  <div class="doc-visio__image doc-photo doc-photo--1"></div>
  <button class="btn btn--icone btn--rond btn--verre-sombre doc-visio__nav doc-visio__nav--d" aria-label="Suivant"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg></button>
  <div class="doc-visio__pellicule">
    <span class="doc-photo doc-photo--2"></span><span class="doc-photo doc-photo--3"></span>
    <span class="doc-photo doc-photo--1 is-actif"></span><span class="doc-photo doc-photo--4"></span>
    <span class="doc-photo doc-photo--5"></span><span class="doc-photo doc-photo--6"></span>
  </div>
</div>
.doc-visio {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 320px;
  padding: 52px var(--sp-12) 78px;
  border-radius: var(--r-md);
  background: rgba(6,8,12,.94);
  overflow: hidden;
}
.doc-visio__barre {
  position: absolute;
  inset: 0 0 auto 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  background: linear-gradient(rgba(0,0,0,.55), transparent);
}
.doc-visio__titre { font-size: var(--fs-sm); font-weight: var(--fw-semi); color: #fff; }
.doc-visio__titre small { color: rgba(255,255,255,.65); font-weight: var(--fw-normal); margin-left: 6px; }
.doc-visio__outils { display: flex; gap: var(--sp-1); }
.doc-visio__image { width: 100%; max-width: 360px; aspect-ratio: 4 / 3; border-radius: var(--r-sm); box-shadow: 0 12px 40px rgba(0,0,0,.5); }
.doc-visio__nav { position: absolute; top: 50%; transform: translateY(-50%); }
.doc-visio__nav--g { left: var(--sp-3); }
.doc-visio__nav--d { right: var(--sp-3); }
.doc-visio__pellicule {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  gap: var(--sp-2);
  padding: var(--sp-3);
  overflow-x: auto;
  justify-content: center;
}
.doc-visio__pellicule span {
  width: 48px; height: 36px;
  flex: none;
  border-radius: var(--r-xs);
  opacity: .5;
  cursor: pointer;
  transition: opacity var(--t-fast), box-shadow var(--t-fast);
}
.doc-visio__pellicule span:hover { opacity: .8; }
.doc-visio__pellicule span.is-actif { opacity: 1; box-shadow: 0 0 0 2px #fff; }
fixed inset-0 z-50 flex items-center justify-center bg-black/90
Panneau de detail

A cote de la grille : l'apercu et les metadonnees du fichier selectionne, sans quitter la liste. Sur mobile, il devient un tiroir du bas.

Voir le codeMasquer le code
<aside class="doc-panneau">
  <div class="doc-panneau__apercu doc-photo doc-photo--3"></div>
  <h4>Local technique.jpg</h4>
  <dl class="doc-panneau__infos">
    <dt>Type</dt><dd>Image JPEG</dd>
    <dt>Taille</dt><dd>1,8 Mo - 4032 x 3024</dd>
    <dt>Ajoute le</dt><dd>12 aout 2026 a 14:32</dd>
    <dt>Par</dt><dd>Julien K.</dd>
    <dt>Dossier</dt><dd>2026-103 - Photos chantier</dd>
  </dl>
  <div class="doc-panneau__actions">
    <button class="btn btn--plein btn--bloc"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg> Telecharger</button>
    <button class="btn btn--secondaire btn--bloc"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/></svg> Copier le lien</button>
  </div>
</aside>
.doc-panneau {
  width: 100%;
  max-width: 300px;
  padding: var(--sp-4);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
}
.doc-panneau__apercu { aspect-ratio: 4 / 3; border-radius: var(--r-md); margin-bottom: var(--sp-3); }
.doc-panneau h4 { margin: 0 0 var(--sp-3); font-size: var(--fs-md); font-weight: var(--fw-bold); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.doc-panneau__infos {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 7px var(--sp-3);
  margin: 0 0 var(--sp-4);
  font-size: var(--fs-sm);
}
.doc-panneau__infos dt { color: var(--c-text-muted); }
.doc-panneau__infos dd { margin: 0; color: var(--c-text); font-weight: var(--fw-medium); }
.doc-panneau__actions { display: flex; flex-direction: column; gap: var(--sp-2); }
w-72 shrink-0 rounded-xl border bg-white p-4 [&_dl]:grid [&_dl]:grid-cols-[auto_1fr] [&_dl]:gap-x-3
Devis LTP-2026-103.pdf 2 / 4
Apercu de document

Lecture d'un PDF sans le telecharger. La barre porte la pagination et le zoom ; le telechargement reste offert, on ne le retire jamais.

Voir le codeMasquer le code
<div class="doc-lecteur">
  <div class="doc-lecteur__barre">
    <span>Devis LTP-2026-103.pdf</span>
    <span class="doc-lecteur__pages">
      <button class="btn btn--icone btn--fantome btn--sm" aria-label="Page precedente"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg></button>
      <em>2 / 4</em>
      <button class="btn btn--icone btn--fantome btn--sm" aria-label="Page suivante"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg></button>
    </span>
    <button class="btn btn--secondaire btn--dense"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg> Telecharger</button>
  </div>
  <div class="doc-lecteur__feuille">
    <div class="doc-page doc-page--rouge"><div class="doc-page__lignes"></div></div>
  </div>
</div>
.doc-lecteur { width: 100%; border: 1px solid var(--c-border); border-radius: var(--r-lg); overflow: hidden; background: var(--c-surface-3); }
.doc-lecteur__barre {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  background: var(--c-surface);
  border-bottom: 1px solid var(--c-border);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semi);
}
.doc-lecteur__barre > span:first-child {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.doc-lecteur__pages { display: inline-flex; align-items: center; gap: var(--sp-1); flex: none; }
.doc-lecteur__pages em { font-style: normal; font-size: var(--fs-xs); color: var(--c-text-muted); min-width: 40px; text-align: center; }
.doc-lecteur__feuille { padding: var(--sp-5); display: flex; justify-content: center; }
.doc-lecteur__feuille .doc-page {
  width: 190px;
  aspect-ratio: 1 / 1.414;
  height: auto;
  border-radius: 2px;
  box-shadow: 0 4px 20px rgba(0,0,0,.18);
}
rounded-xl border bg-slate-100 [&_.feuille]:mx-auto [&_.feuille]:aspect-[1/1.414] [&_.feuille]:bg-white [&_.feuille]:shadow-lg
Avant
Apres
Avant / apres

La preuve de chantier. Le curseur central revele l'une ou l'autre image ; en dessous, deux vignettes cote a cote suffisent souvent.

Voir le codeMasquer le code
<div class="doc-avant-apres">
  <div class="doc-avant-apres__cote doc-photo doc-photo--6"><span>Avant</span></div>
  <div class="doc-avant-apres__cote doc-photo doc-photo--3"><span>Apres</span></div>
</div>
.doc-avant-apres { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); width: 100%; }
.doc-avant-apres__cote { position: relative; aspect-ratio: 4 / 3; border-radius: var(--r-md); overflow: hidden; }
.doc-avant-apres__cote span {
  position: absolute;
  left: var(--sp-2); bottom: var(--sp-2);
  padding: 4px 10px;
  border-radius: var(--r-full);
  background: rgba(10,12,18,.62);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: #fff;
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
}
grid grid-cols-2 gap-2 [&>div]:relative [&>div]:aspect-[4/3] [&_span]:absolute [&_span]:bottom-2 [&_span]:left-2

F. Etats et outils

Vide, chargement, erreur, non previsualisable, selection multiple, filtres. Un etat vide sans bouton d'action est un cul-de-sac.

Aucun document dans ce dossier

Ajoutez le devis, les photos de chantier et les attestations. Le client les retrouvera dans son espace.

Aucun document

Premiere visite d'un dossier vide. Trois elements obligatoires : ce qui manque, pourquoi c'est utile, et le bouton pour commencer.

Voir le codeMasquer le code
<div class="doc-vide">
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="5" rx="1"/><path d="M5 9v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9M10 13h4"/></svg>
  <strong>Aucun document dans ce dossier</strong>
  <p>Ajoutez le devis, les photos de chantier et les attestations. Le client les retrouvera dans son espace.</p>
  <button class="btn btn--plein"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg> Ajouter des documents</button>
</div>
.doc-vide {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  padding: var(--sp-10) var(--sp-6);
  border: 1px dashed var(--c-border-strong);
  border-radius: var(--r-lg);
  background: var(--c-surface);
  text-align: center;
}
.doc-vide > svg { width: 42px; height: 42px; color: var(--c-text-muted); opacity: .7; }
.doc-vide strong { font-size: var(--fs-lg); font-weight: var(--fw-bold); color: var(--c-text); }
.doc-vide p { margin: 0; max-width: 42ch; font-size: var(--fs-md); color: var(--c-text-soft); line-height: 1.55; }
flex flex-col items-center gap-3 rounded-xl border border-dashed p-10 text-center
Aucun document ne correspond

Aucun resultat pour « attestation 2024 » avec le filtre Photos.

Aucun resultat

Apres un filtre ou une recherche. Different de F1 : ici il y a des documents, ce sont les criteres qui ne rendent rien. Le bouton efface les filtres.

Voir le codeMasquer le code
<div class="doc-vide doc-vide--recherche">
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
  <strong>Aucun document ne correspond</strong>
  <p>Aucun resultat pour <em>&laquo; attestation 2024 &raquo;</em> avec le filtre <em>Photos</em>.</p>
  <button class="btn btn--secondaire">Effacer les filtres</button>
</div>
.doc-vide--recherche em { font-style: normal; font-weight: var(--fw-semi); color: var(--c-text); }
idem F1, message centre sur les criteres et non sur le dossier
Chargement

Squelettes calques sur la grille reelle : meme nombre de colonnes, memes proportions. Sinon la page saute a l'arrivee des donnees.

Voir le codeMasquer le code
<div class="doc-grille">
  <div class="doc-carte"><div class="doc-vignette doc-squelette"></div><span class="doc-squelette doc-squelette--ligne"></span></div>
  <div class="doc-carte"><div class="doc-vignette doc-squelette"></div><span class="doc-squelette doc-squelette--ligne" style="width:70%"></span></div>
  <div class="doc-carte"><div class="doc-vignette doc-squelette"></div><span class="doc-squelette doc-squelette--ligne" style="width:85%"></span></div>
  <div class="doc-carte"><div class="doc-vignette doc-squelette"></div><span class="doc-squelette doc-squelette--ligne" style="width:60%"></span></div>
</div>
.doc-squelette {
  background: linear-gradient(90deg, var(--c-surface-2) 25%, var(--c-surface-3) 50%, var(--c-surface-2) 75%);
  background-size: 200% 100%;
  animation: doc-scintille 1.4s ease-in-out infinite;
  border-radius: var(--r-md);
}
.doc-squelette--ligne { display: block; height: 12px; width: 100%; border-radius: var(--r-xs); }
@keyframes doc-scintille { to { background-position: -200% 0; } }
animate-pulse bg-slate-200
Apercu indisponible
Plan technique.dwg 4,2 Mo - format non pris en charge
Archive photos.zip 42 Mo - 18 fichiers
Apercu impossible

Format non previsualisable, fichier corrompu, trop volumineux. La ligne reste utilisable : le telechargement, lui, doit rester possible.

Voir le codeMasquer le code
<div class="doc-grille">
  <figure class="doc-carte">
    <div class="doc-vignette doc-vignette--erreur"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg><span>Apercu indisponible</span></div>
    <span class="doc-carte__nom">Plan technique.dwg</span>
    <span class="doc-carte__meta">4,2 Mo - format non pris en charge</span>
  </figure>
  <figure class="doc-carte">
    <div class="doc-vignette"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#b45309"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">ZIP</text></svg></div>
    <span class="doc-carte__nom">Archive photos.zip</span>
    <span class="doc-carte__meta">42 Mo - 18 fichiers</span>
  </figure>
</div>
.doc-vignette--erreur {
  flex-direction: column;
  gap: 6px;
  background: var(--c-surface-2);
  color: var(--c-text-muted);
  border: 1px dashed var(--c-border-strong);
}
.doc-vignette--erreur svg { width: 26px; height: 26px; }
.doc-vignette--erreur span { font-size: var(--fs-xs); }
flex-col gap-1 bg-slate-100 text-slate-400
4 documents
Barre de selection

Apparait des qu'une case est cochee. Elle porte le compte, les actions de masse, et une sortie claire de la selection.

Voir le codeMasquer le code
<div class="doc-selection">
  <button class="btn btn--icone btn--fantome btn--sm" aria-label="Annuler la selection"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  <strong>4 documents</strong>
  <span class="btn-barre__sep"></span>
  <button class="btn btn--fantome btn--dense"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg> Telecharger</button>
  <button class="btn btn--fantome btn--dense"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M3 6a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg> Deplacer</button>
  <button class="btn btn--fantome btn--dense"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/></svg> Partager</button>
  <span class="btn-barre__sep"></span>
  <button class="btn btn--fantome btn--dense doc-selection__danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg> Supprimer</button>
</div>
.doc-selection {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 7px var(--sp-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-full);
  background: var(--c-surface);
  box-shadow: var(--sh-3);
}
.doc-selection strong { font-size: var(--fs-sm); font-weight: var(--fw-bold); padding: 0 var(--sp-2); white-space: nowrap; }
.doc-selection__danger { color: var(--c-danger); }
.doc-selection__danger:hover { background: var(--c-danger-soft); color: var(--c-danger); }
@media (max-width: 560px) { .doc-selection { flex-wrap: wrap; border-radius: var(--r-lg); justify-content: center; } }
fixed bottom-6 left-1/2 -translate-x-1/2 flex items-center gap-1 rounded-full border bg-white px-3 py-2 shadow-xl
Photos Aout 2026
Filtres de galerie

Au-dessus de la liste : recherche, type, periode. Les filtres actifs restent affiches en etiquettes retirables, sinon on oublie pourquoi la liste est vide.

Voir le codeMasquer le code
<div class="doc-filtres">
  <div class="doc-filtres__ligne">
    <label class="doc-recherche"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg><input type="search" placeholder="Rechercher un document"></label>
    <button class="btn btn--secondaire"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 4h18l-7 8v6l-4 2v-8Z"/></svg> Type <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button>
    <button class="btn btn--secondaire"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg> Periode <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button>
  </div>
  <div class="doc-filtres__actifs">
    <span class="btn-etiquette btn-etiquette--accent">Photos <button aria-label="Retirer le filtre Photos"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></span>
    <span class="btn-etiquette btn-etiquette--accent">Aout 2026 <button aria-label="Retirer le filtre Aout 2026"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></span>
    <button class="btn btn--texte btn--dense">Tout effacer</button>
  </div>
</div>
.doc-filtres { display: flex; flex-direction: column; gap: var(--sp-3); width: 100%; }
.doc-filtres__ligne { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.doc-filtres__actifs { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.doc-recherche { position: relative; display: flex; align-items: center; flex: 1; min-width: 190px; }
.doc-recherche svg { position: absolute; left: 11px; width: 17px; height: 17px; color: var(--c-text-muted); pointer-events: none; }
.doc-recherche input {
  width: 100%;
  height: var(--h-md);
  padding: 0 var(--sp-3) 0 36px;
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-surface);
  color: var(--c-text);
  font-family: var(--font-ui);
  font-size: var(--fs-md);
}
.doc-recherche input::placeholder { color: var(--c-text-muted); }
.doc-recherche input:focus { outline: 2px solid var(--c-focus); outline-offset: -1px; border-color: var(--c-primary); }
flex flex-col gap-3 [&_input]:h-10 [&_input]:w-full [&_input]:rounded-lg [&_input]:border [&_input]:pl-9

G. Cas metier

Assemblages complets prets a coller : documents d'un dossier, photos avant / apres, versions, partage, galerie mobile.

Documents d'un dossier

L'assemblage type d'une fiche client de CRM : onglets par categorie, compte par onglet, depot toujours accessible.

Voir le codeMasquer le code
<div class="doc-bloc">
  <div class="doc-bloc__onglets">
    <button class="btn btn--fantome btn--onglet is-actif">Tous <span class="btn__badge">12</span></button>
    <button class="btn btn--fantome btn--onglet">Devis <span class="btn__badge">3</span></button>
    <button class="btn btn--fantome btn--onglet">Photos <span class="btn__badge">7</span></button>
    <button class="btn btn--fantome btn--onglet">Administratif <span class="btn__badge">2</span></button>
    <span class="cat-head__spacer"></span>
    <button class="btn btn--plein btn--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg> Ajouter</button>
  </div>
  <ul class="doc-liste">
    <li><a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#e8453c"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">PDF</text></svg><span class="doc-liste__bloc"><strong>Devis LTP-2026-103.pdf</strong><small>12 aout - 248 Ko</small></span><span class="doc-etiq doc-etiq--ok">Signe</span></a></li>
    <li><a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#7c3aed"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">JPG</text></svg><span class="doc-liste__bloc"><strong>Photo toiture.jpg</strong><small>9 aout - 1,8 Mo</small></span><span class="doc-etiq doc-etiq--neutre">Photo</span></a></li>
    <li><a href="#"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="#2b579a"/><path d="M14 2v6h6" fill="#fff" fill-opacity=".35"/><text x="12" y="17" font-family="system-ui,sans-serif" font-size="6.5" font-weight="700" fill="#fff" text-anchor="middle">DOC</text></svg><span class="doc-liste__bloc"><strong>Mandat MaPrimeRenov.docx</strong><small>9 aout - 84 Ko</small></span><span class="doc-etiq doc-etiq--attente">A signer</span></a></li>
  </ul>
</div>
.doc-bloc { width: 100%; border: 1px solid var(--c-border); border-radius: var(--r-lg); background: var(--c-surface); overflow: hidden; }
.doc-bloc__onglets {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-2) var(--sp-3) 0;
  border-bottom: 1px solid var(--c-border);
  overflow-x: auto;
}
.doc-bloc__onglets .btn--sm { margin-bottom: var(--sp-2); flex: none; }
.doc-bloc__onglets .btn--onglet { flex: none; }
.doc-bloc .doc-liste { border: none; border-radius: 0; }
rounded-xl border bg-white [&_nav]:flex [&_nav]:gap-1 [&_nav]:border-b [&_nav]:px-3
12 aout 2026 Pose de l'unite exterieure
14:32
15:07
16:45
9 aout 2026 Visite technique
10:12
10:26
Photos de chantier datees

La galerie d'un artisan : les photos regroupees par visite, avec la date en tete. Sert de preuve en cas de litige.

Voir le codeMasquer le code
<div class="doc-chantier">
  <div class="doc-chantier__jour">
    <h5><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg> 12 aout 2026 <small>Pose de l'unite exterieure</small></h5>
    <div class="doc-grille">
      <figure class="doc-carte"><div class="doc-vignette doc-photo doc-photo--3"><span class="doc-etat doc-etat--info">14:32</span></div></figure>
      <figure class="doc-carte"><div class="doc-vignette doc-photo doc-photo--4"><span class="doc-etat doc-etat--info">15:07</span></div></figure>
      <figure class="doc-carte"><div class="doc-vignette doc-photo doc-photo--6"><span class="doc-etat doc-etat--info">16:45</span></div></figure>
    </div>
  </div>
  <div class="doc-chantier__jour">
    <h5><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg> 9 aout 2026 <small>Visite technique</small></h5>
    <div class="doc-grille">
      <figure class="doc-carte"><div class="doc-vignette doc-photo doc-photo--1"><span class="doc-etat doc-etat--info">10:12</span></div></figure>
      <figure class="doc-carte"><div class="doc-vignette doc-photo doc-photo--2"><span class="doc-etat doc-etat--info">10:26</span></div></figure>
    </div>
  </div>
</div>
.doc-chantier { width: 100%; display: flex; flex-direction: column; gap: var(--sp-6); }
.doc-chantier__jour h5 {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  color: var(--c-text);
}
.doc-chantier__jour h5 svg { width: 17px; height: 17px; color: var(--c-primary); }
.doc-chantier__jour h5 small { font-size: var(--fs-sm); font-weight: var(--fw-normal); color: var(--c-text-muted); }
space-y-6 [&_h5]:flex [&_h5]:items-center [&_h5]:gap-2 [&_h5]:text-sm [&_h5]:font-bold
  1. v3 Devis LTP-2026-103.pdf12 aout 2026 - Julien K. - version en vigueur Actuelle
  2. v2 Devis LTP-2026-103.pdf9 aout 2026 - Julien K. - ajout de l'option ballon
  3. v1 Devis LTP-2026-103.pdf5 aout 2026 - Sophie M. - creation
Historique de versions

Un meme document remplace plusieurs fois. La version courante est en tete, les precedentes restent consultables et jamais supprimees en silence.

Voir le codeMasquer le code
<ol class="doc-versions">
  <li class="is-actuelle">
    <span class="doc-versions__pastille">v3</span>
    <span class="doc-liste__bloc"><strong>Devis LTP-2026-103.pdf</strong><small>12 aout 2026 - Julien K. - version en vigueur</small></span>
    <span class="doc-etiq doc-etiq--ok">Actuelle</span>
  </li>
  <li>
    <span class="doc-versions__pastille">v2</span>
    <span class="doc-liste__bloc"><strong>Devis LTP-2026-103.pdf</strong><small>9 aout 2026 - Julien K. - ajout de l'option ballon</small></span>
    <button class="btn btn--fantome btn--dense">Restaurer</button>
  </li>
  <li>
    <span class="doc-versions__pastille">v1</span>
    <span class="doc-liste__bloc"><strong>Devis LTP-2026-103.pdf</strong><small>5 aout 2026 - Sophie M. - creation</small></span>
    <button class="btn btn--fantome btn--dense">Restaurer</button>
  </li>
</ol>
.doc-versions { list-style: none; margin: 0; padding: 0; width: 100%; position: relative; }
/* Le trait vertical qui relie les versions, tire entre la premiere et la derniere pastille. */
.doc-versions::before {
  content: "";
  position: absolute;
  left: 15px;
  top: 28px;
  bottom: 28px;
  width: 2px;
  background: var(--c-border);
}
.doc-versions li {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) 0;
}
.doc-versions__pastille {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px; height: 32px;
  flex: none;
  border-radius: var(--r-full);
  background: var(--c-surface-3);
  border: 2px solid var(--c-surface);
  color: var(--c-text-soft);
  font-size: 11px;
  font-weight: var(--fw-bold);
  z-index: 1;
}
.doc-versions li.is-actuelle .doc-versions__pastille { background: var(--c-primary); color: var(--c-primary-text); }
.doc-versions li.is-actuelle strong { color: var(--c-text); }
.doc-versions li:not(.is-actuelle) { opacity: .78; }
relative [&_li]:flex [&_li]:items-center [&_li]:gap-3 [&_li]:py-3 before:absolute before:left-4 before:h-full before:w-px before:bg-slate-200

Partager 4 documents

jabsdev.fr/p/a7f3c9
Partage par lien

Envoyer un dossier a un client sans lui creer de compte. Trois reglages suffisent : qui peut ouvrir, jusqu'a quand, et avec quel droit.

Voir le codeMasquer le code
<div class="doc-partage">
  <h4><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/></svg> Partager 4 documents</h4>
  <div class="btn-copier doc-partage__lien">
    <code>jabsdev.fr/p/a7f3c9</code>
    <button class="btn btn--icone btn--fantome btn--sm" aria-label="Copier le lien"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
  </div>
  <label class="bascule-ligne">
    <span class="bascule-ligne__texte"><strong>Autoriser le telechargement</strong><small>Sinon le client peut seulement consulter</small></span>
    <input type="checkbox" checked><span class="bascule__piste"></span>
  </label>
  <label class="bascule-ligne">
    <span class="bascule-ligne__texte"><strong>Expiration dans 30 jours</strong><small>Le lien cessera de fonctionner le 5 octobre</small></span>
    <input type="checkbox" checked><span class="bascule__piste"></span>
  </label>
  <button class="btn btn--plein btn--bloc">Envoyer par courriel</button>
</div>
.doc-partage {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  width: 100%;
  max-width: 420px;
  padding: var(--sp-4);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
}
.doc-partage h4 { display: flex; align-items: center; gap: var(--sp-2); margin: 0; font-size: var(--fs-lg); font-weight: var(--fw-bold); }
.doc-partage h4 svg { width: 18px; height: 18px; color: var(--c-primary); }
.doc-partage__lien { width: 100%; justify-content: space-between; }
.doc-partage__lien code { overflow: hidden; text-overflow: ellipsis; }
flex w-full max-w-md flex-col gap-3 rounded-xl border bg-white p-4
Galerie mobile

Defilement horizontal avec calage : chaque photo s'aligne d'elle-meme. La barre de position remplace des fleches inutilisables au doigt.

Voir le codeMasquer le code
<div class="doc-carrousel">
  <span class="doc-photo doc-photo--1"></span>
  <span class="doc-photo doc-photo--2"></span>
  <span class="doc-photo doc-photo--3"></span>
  <span class="doc-photo doc-photo--4"></span>
  <span class="doc-photo doc-photo--5"></span>
</div>
.doc-carrousel {
  display: flex;
  gap: var(--sp-3);
  width: 100%;
  padding-bottom: var(--sp-2);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: thin;
  -webkit-overflow-scrolling: touch;
}
.doc-carrousel > * {
  flex: none;
  width: 180px;
  aspect-ratio: 4 / 3;
  border-radius: var(--r-md);
  scroll-snap-align: start;
}
flex snap-x snap-mandatory gap-3 overflow-x-auto [&>*]:snap-start [&>*]:shrink-0

Devis LTP-2026-103

4 280,00 EUR TTC

Signature electronique horodatee, conforme au reglement eIDAS.
Document a signer

L'ecran de signature d'un devis : l'apercu, la mention legale, puis l'action. Le bouton reste bloque tant que la case n'est pas cochee.

Voir le codeMasquer le code
<div class="doc-signature">
  <div class="doc-signature__apercu"><div class="doc-page doc-page--rouge"><div class="doc-page__lignes"></div></div></div>
  <div class="doc-signature__corps">
    <h4>Devis LTP-2026-103</h4>
    <p class="doc-signature__montant">4 280,00 EUR TTC</p>
    <label class="coche"><input type="checkbox"><span class="coche__boite"></span> J'ai lu et j'accepte les conditions generales de vente</label>
    <span class="btn-explique" data-bulle="Cochez d'abord les conditions generales">
      <button class="btn btn--plein btn--lg btn--bloc" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 17c3 0 3-9 6-9s3 9 6 9 3-5 6-5"/><path d="M3 21h18"/></svg> Signer le devis</button>
    </span>
    <small>Signature electronique horodatee, conforme au reglement eIDAS.</small>
  </div>
</div>
.doc-signature {
  display: flex;
  gap: var(--sp-4);
  width: 100%;
  max-width: 520px;
  padding: var(--sp-4);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
}
.doc-signature__apercu { width: 120px; flex: none; }
.doc-signature__apercu .doc-page { aspect-ratio: 1 / 1.414; height: auto; border-radius: var(--r-sm); box-shadow: var(--sh-2); }
.doc-signature__corps { display: flex; flex-direction: column; gap: var(--sp-3); flex: 1; min-width: 0; }
.doc-signature__corps h4 { margin: 0; font-size: var(--fs-lg); font-weight: var(--fw-bold); }
.doc-signature__montant { margin: 0; font-size: 24px; font-weight: 800; letter-spacing: -.02em; color: var(--c-text); }
.doc-signature__corps small { font-size: var(--fs-xs); color: var(--c-text-muted); line-height: 1.5; }
.doc-signature .btn-explique { display: block; }
@media (max-width: 560px) {
  .doc-signature { flex-direction: column; }
  .doc-signature__apercu { width: 100px; }
}
flex gap-4 rounded-xl border bg-white p-4 max-sm:flex-col