Merge pull request #178 from IliesAllali/feat/squelettes

Blocs gris pendant le chargement (onglet Vie, panneaux de l'arbre)
This commit is contained in:
IliesAllali authored and GitHub committed 2026-10-27 17:15:18 +01:00
commit 0f98b8b507
9 files changed
+63 -5

No files matched your search

+2 -1
View File
@@ -24,6 +24,7 @@ import { removeManager } from '../api/managersApi.js'
import { t } from '../i18n/index.js'
import { readGedcomFile } from '../utils/gedcomText.js'
import { readGedcomBundle, zipErrorMessage } from '../utils/gedcomBundle.js'
import Skeleton from './Skeleton.jsx'
function normalizeSlug(value) {
return String(value || '')
@@ -442,7 +443,7 @@ const AdminPanel = ({
<div className="ts-content">
{loading ? (
<p className="pz-small ts-loading">{t('Chargement…')}</p>
<Skeleton variant="lines" count={4} />
) : (
<>
{errorMessage && <div className="pz-error" role="alert">{errorMessage}</div>}
@@ -8,6 +8,7 @@ import PzBusy from './PzBusy.jsx'
import { HELP_URL } from '../utils/siteLinks.js'
import { t, dateLocale } from '../i18n/index.js'
import { sexLabel } from '../utils/personSex.js'
import Skeleton from './Skeleton.jsx'
// ─── helpers ────────────────────────────────────────────────────────────────
@@ -471,7 +472,7 @@ const ContributionPanel = ({
{errorMessage && <div className="contrib-error">{errorMessage}</div>}
{loading ? (
<div className="contrib-empty">{t('Chargement…')}</div>
<Skeleton variant="rows" count={3} />
) : sessions.length === 0 ? (
<div className="contrib-empty">{t("Rien à relire pour l'instant. Les propositions de la famille apparaîtront ici.")}</div>
) : (
+4
View File
@@ -4,6 +4,7 @@ import { t } from '../i18n/index.js'
import { buildLifeTimeline } from '../utils/lifeTimeline.js'
import './LifeTimeline.css'
import { blurPlaceholder } from '../utils/blurhash.js'
import Skeleton from './Skeleton.jsx'
// Onglet Vie de la fiche : naissance, unions et décès en grands repères, les autres événements
// plus petits entre eux, la famille proche en retrait. Les sources restent repliées.
@@ -71,6 +72,9 @@ export default function LifeTimeline({
const noticeLine = notice && <p className="pz-success lt-notice" role="status">{notice}</p>
if (entries.length === 0) {
// Premier chargement, aucune date connue sur la fiche : la place de la frise, au lieu d'un « pas encore de
// date » qui serait faux une seconde après. Avec des dates connues, elles s'affichent tout de suite.
if (loading && !life) return <Skeleton variant="timeline" count={3} label={t('Chargement de la frise')} />
return (
<div className="pc-empty">
<p className="pz-sub">{t('Pas encore de date connue pour {name}.', { name: person.firstName })}</p>
@@ -8,6 +8,7 @@ import { fetchSearchSources, searchSource } from '../api/searchSourcesApi.js'
import { SEARCH_LINKS, SEARCH_LINK_REGIONS } from '../data/searchLinks.js'
import { changesFromResult, personFromResult } from '../utils/searchResultChanges.js'
import { fullNameLabel } from '../utils/personName.js'
import Skeleton from './Skeleton.jsx'
// Ce que chaque source interrogée couvre et ce qui part vers elle : affiché avant qu'on coche
const SOURCE_INFO = {
@@ -283,7 +284,7 @@ function SearchSourcesPanel({ visible, treeId, token, persons = [], canApply = f
<div className="apn-section sp-sources">
<p className="pz-label">{t('Où chercher')}</p>
{sources === null && <p className="pz-hint">{t('Chargement…')}</p>}
{sources === null && <Skeleton variant="lines" count={3} />}
{sources?.length === 0 && (
<p className="pz-hint">{t("Ce serveur n'interroge aucune source : aucun nom ne quitte l'instance. Les liens plus bas ouvrent les sites dans votre navigateur.")}</p>
)}
+14
View File
@@ -0,0 +1,14 @@
/* Blocs gris de chargement (Skeleton.jsx) : immobiles, couleur des séparateurs */
.pz-skel { display: grid; gap: 14px; padding: 4px 0; }
.pz-skel-row { display: flex; align-items: center; gap: 12px; }
.pz-skel-text { display: grid; gap: 7px; flex: 1; min-width: 0; }
.pz-skel-bar { display: block; height: 12px; border-radius: 6px; background: var(--pz-line); }
.pz-skel-bar--thin { height: 9px; background: var(--pz-line-soft); }
.pz-skel-dot { flex: none; width: 32px; height: 32px; border-radius: 50%; background: var(--pz-line); }
/* Frise : année, point sur le rail, texte */
.pz-skel--timeline { gap: 22px; padding: 8px 0 4px; }
.pz-skel--timeline .pz-skel-year { flex: none; width: 34px; height: 11px; }
.pz-skel--timeline .pz-skel-dot { width: 10px; height: 10px; }
.pz-skel--lines .pz-skel-bar { height: 10px; }
+34
View File
@@ -0,0 +1,34 @@
import { useEffect, useState } from 'react'
import { t } from '../i18n/index.js'
import './Skeleton.css'
// Blocs gris à la forme de ce qui arrive, à la place d'un « Chargement… ». Rien avant SHOW_AFTER : une réponse
// rapide ne fait pas clignoter l'écran. Aucun mouvement (pas de reflet qui passe), juste la place réservée.
const SHOW_AFTER = 300
// variant : rows (avatar et deux lignes : personnes, visites, propositions), timeline (frise de l'onglet Vie),
// lines (paragraphes)
export default function Skeleton({ variant = 'rows', count = 3, label = '' }) {
const [shown, setShown] = useState(false)
useEffect(() => {
const timer = setTimeout(() => setShown(true), SHOW_AFTER)
return () => clearTimeout(timer)
}, [])
return (
<div className={`pz-skel pz-skel--${variant}`} role="status" aria-busy="true">
<span className="pz-sr">{label || t('Chargement…')}</span>
{shown && Array.from({ length: count }, (_, i) => (
<div key={i} className="pz-skel-row" aria-hidden="true">
{variant === 'rows' && <span className="pz-skel-dot" />}
{variant === 'timeline' && <span className="pz-skel-bar pz-skel-year" />}
{variant === 'timeline' && <span className="pz-skel-dot" />}
<span className="pz-skel-text">
<span className="pz-skel-bar" style={{ width: `${[62, 48, 70, 55][i % 4]}%` }} />
{variant !== 'lines' && <span className="pz-skel-bar pz-skel-bar--thin" style={{ width: `${[38, 30, 44, 34][i % 4]}%` }} />}
</span>
</div>
))}
</div>
)
}
@@ -3,6 +3,7 @@ import { inviteManager, invitationUrl, listManagers, removeManager, renewInvitat
import PzBusy from './PzBusy.jsx'
import { dateLocale, t } from '../i18n/index.js'
import { initialOf } from '../utils/initial.js'
import Skeleton from './Skeleton.jsx'
// Onglet Gestion des paramètres de l'arbre (issue #145). Le propriétaire invite des co-gestionnaires par leur
// adresse : Parenthèse n'envoie pas de mail, il donne un lien à transmettre. Les co-gestionnaires voient la liste.
@@ -102,7 +103,7 @@ const TreeManagersSection = ({ treeId, authToken, currentUserId = null, oidcName
}
}
if (state.loading) return <p className="pz-small ts-loading">{t('Chargement…')}</p>
if (state.loading) return <Skeleton variant="rows" count={2} />
if (state.error) return <div className="pz-error" role="alert">{state.error}</div>
const { canManage, managers, invitations } = state.data
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { listTreeVisits } from '../api/treeApi'
import { dateLocale, t } from '../i18n/index.js'
import Skeleton from './Skeleton.jsx'
// Onglet Visites des paramètres de l'arbre : qui est venu, combien de fois.
// Le journal est tenu par le serveur à chaque ouverture de l'arbre (backend/src/lib/tree-visits.ts).
@@ -37,7 +38,7 @@ const TreeVisitsSection = ({ treeId, authToken }) => {
return () => { active = false }
}, [treeId, authToken])
if (state.loading) return <p className="pz-small ts-loading">{t('Chargement…')}</p>
if (state.loading) return <Skeleton variant="rows" count={4} />
if (state.error) return <div className="pz-error" role="alert">{state.error}</div>
const { summary, recent } = state.data
+1
View File
@@ -432,6 +432,7 @@ export default {
'Naissance de {name}': 'Birth of {name}',
'Décès à {place}': 'Died in {place}',
'Ajouter un moment': 'Add a moment',
'Chargement de la frise': 'Loading the timeline',
'Modifier « {title} »': 'Edit “{title}”',
'Déménagement': 'Move',
'L’enregistrement n’a pas abouti. Réessayez dans un instant.': 'Saving did not go through. Try again in a moment.',