Skip to content

Le numéro d'un bénévole se distingue, et se lit à voix haute - #57

Merged
flocom merged 1 commit into
mainfrom
benevoles-coordonnees-lisibles
Sep 19, 2026
Merged

flocom merged 1 commit into
mainfrom
benevoles-coordonnees-lisibles

Conversation

@flocom

@flocom flocom commented Sep 19, 2026

Copy link
Copy Markdown
Owner

Suite de « Les coordonnées des bénévoles se voient enfin » (75fa224), qui avait supprimé le pli et remonté le contraste. Cette PR traite ce qui restait, vérifié sur l'écran tel qu'il est aujourd'hui — pas sur celui d'avant.

Ce qui était encore cassé

Constat État avant Après
Téléphone et e-mail indistinguables tous deux text-xs text-slate-600 téléphone devant, text-sm text-brand-700 (6,78:1) + icône ; e-mail text-xs text-slate-600 (7,24:1) + icône
Liens sans nom accessible « lien, +33634580545 » préfixe sr-only « Appeler Marie au … »
« Retirer » aucune hauteur minimale, ~20 px min-h-11 min-w-11
Modifier / Dupliquer / Supprimer min-h-8 (32 px) min-h-11 (44 px)
Débordement break-all sur l'e-mail, rien sur le téléphone [overflow-wrap:anywhere] sur nom, téléphone et e-mail
Badge « invité » (réunion) slate-100/slate-500 = 4,34:1, sous AA slate-200/slate-600 = 6,15:1

Pourquoi sr-only et pas aria-label

Un aria-label remplace le nom accessible. Poser aria-label="Appeler Marie Dupont" sur un lien dont le texte visible est le numéro retire ce numéro de ce qui est annoncé : échec du critère WCAG 2.5.3 « Label in Name » (niveau A), et la commande vocale ne reconnaît plus ce que l'utilisateur lit à l'écran. Le préfixe sr-only donne « Appeler Marie Dupont au +33634580545 » — le texte visible reste contenu dans le nom accessible. C'est le motif que le bouton « Retirer » utilisait déjà.

Pourquoi [overflow-wrap:anywhere] et pas break-words

break-words ne participe pas au calcul de min-content : un e-mail ou un numéro sans point de coupure pousse la carte hors de la fenêtre malgré max-w-full. anywhere y participe, tout en ne coupant qu'en dernier recours — contrairement à break-all, qui scindait « +33634580545 » entre deux chiffres. phone n'est validé que par z.string().trim().max(40) : 40 caractères sans séparateur sont acceptés, et ne débordent plus.

Le coût, chiffré

Déplier coûte de la hauteur : douze inscrits font environ 1 300 px sur une colonne. Repris en largeur — sm:grid-cols-2 xl:grid-cols-3 — plutôt qu'en repliant à nouveau ce qu'on vient de montrer. Largeur de colonne réelle mesurée, barre latérale lg:w-[18rem] comprise : ~282 px à 1280 px, ~272 px à 640 px, ~232 px de texte à 320 px.

Deux échecs silencieux

  • Le bloc d'erreur n'était rendu que sous canManage && showAdd, alors que duplicateSlot, saveSlot, deleteSlot et removeSignup alimentent le même error. Une duplication qui échouait ne disait rien. Il sort du formulaire.
  • deleteSlot et removeSignup n'attrapaient même pas leur exception. Elles la signalent.

Frontière public/privé

Rien de nouveau ne traverse. La projection serveur→client (page.tsx l.155-168) reste la liste blanche { id, name, email, phone } — pas de spread, pas de cancelToken, pas de userId, pas de createdAt. phone était déjà sérialisé dans la charge RSC : cette PR le rend lisible à qui a déjà le droit de le lire. SlotManager n'a toujours qu'un seul montage (dashboard/events/[id]/page.tsx), sous le requireUser() du layout. Aucun composant partagé n'est extrait — le seul garde-fou structurel est qu'un composant sachant afficher un téléphone d'inscrit n'existe qu'à un endroit, importé une fois.

Une fuite refermée en plus. getUpcomingPublishedEvents() chargeait encore la ligne d'inscription entière — nom, e-mail, téléphone et jeton d'annulation — pour l'accueil / et /rejoindre, qui n'en lisent que .length (vérifié : page.tsx l.55 et l.226, rejoindre l.209 et l.222). Elle ne charge plus que l'identifiant. Aucun effet visible ; la fuite devient impossible par construction plutôt que par discipline. C'était la dernière des trois lectures publiques encore ouverte.

Ce qui n'est PAS fait, volontairement

Les événements lisent toujours signup.phone brut, sans le repli vers la fiche d'adhérent que les réunions obtiennent par telephonePresent(). Un membre connecté qui s'inscrit sans retaper son numéro apparaît donc sans téléphone, alors que l'association le connaît. Le corriger demande de joindre associationMember dans getEventWithDetails, qui a cinq appelants — dont l'outil MCP get_event, qui fait toolResult({ event }) et relaierait donc le numéro privé d'un adhérent. C'est un autre changement, avec sa propre revue.

Point à trancher séparément, signalé sans être décidé ici : /dashboard/events/[id] n'exige que requireUser(). Ces coordonnées sont visibles de tout membre connecté, pas seulement d'un manager — c'était déjà vrai avant, le pli ne protégeait rien puisque la donnée était déjà dans le HTML. Restreindre serait une décision de produit, côté serveur, pas un effet de bord d'une correction d'accessibilité.

Vérification

  • npx tsc --noEmit : 0 erreur
  • npx next lint : aucun avertissement
  • npx next build : succès
  • Utilitaires Tailwind confirmés dans le CSS généré : min-w-11, overflow-wrap:anywhere, .xl\:grid-cols-3 sous min-width:1280px, gap-1.5
  • Les 18 ratios de contraste recalculés (WCAG 2.1, luminance relative) sur les hex exacts de tailwind.config.ts

🤖 Generated with Claude Code

https://claude.ai/code/session_014SfQYBU4xXTeSEHKhHQXdD


Generated by Claude Code

Le pli et le gris ont été corrigés au commit précédent : les inscrits
s'affichent sous leur créneau, coordonnées cliquables à 7,24:1. Restaient
quatre choses, vérifiées sur l'écran tel qu'il est aujourd'hui.

Le téléphone ne se distinguait pas de l'adresse. Les deux sortaient en
text-xs slate-600, l'un sous l'autre, sans rien pour dire lequel on vient
chercher. Le numéro passe devant, en text-sm brand-700 (6,78:1), avec son
icône ; l'adresse reste en text-xs slate-600 (7,24:1), avec la sienne.

Les liens n'avaient pas de nom. Un lecteur d'écran annonçait « lien,
+33634580545 » sans dire à qui. Le nom est préfixé en sr-only plutôt que
posé en aria-label : un aria-label aurait REMPLACÉ le nom accessible,
donc retiré le numéro visible de ce qui est annoncé, et cassé la commande
vocale — c'est le critère 2.5.3 « Label in Name ». Le motif est celui que
le bouton « Retirer » utilisait déjà.

Trois cibles restaient sous 44 px, alors que le commit précédent
annonçait le contraire : « Retirer » n'avait aucune hauteur minimale
(~20 px), et Modifier / Dupliquer / Supprimer étaient à min-h-8.

Le débordement n'était traité qu'à moitié. L'adresse portait `break-all`,
qui coupe n'importe où — y compris un numéro entre deux chiffres — et le
téléphone ne portait rien du tout, alors qu'il accepte 40 caractères sans
séparateur. Les deux passent à `[overflow-wrap:anywhere]`, qui ne coupe
qu'en dernier recours et qui, contrairement à `break-words`, entre dans
le calcul de min-content : c'est ce qui empêche la carte de sortir de
l'écran à 320 px. Le nom le reçoit aussi.

Déplier coûte de la hauteur — douze inscrits font environ 1 300 px sur
une colonne. Elle est reprise en largeur, deux colonnes dès 640 px et
trois dès 1280 px, plutôt qu'en repliant à nouveau ce qu'on vient de
montrer.

Deux échecs silencieux au passage : le bloc d'erreur n'était rendu que si
le formulaire d'ajout était ouvert, alors que la duplication, la
suppression d'un créneau et le retrait d'un inscrit alimentent le même
`error` — une duplication qui échouait ne disait rien. Il sort du
formulaire, et les deux suppressions, qui n'attrapaient même pas leur
exception, la signalent.

Côté réunion, le même traitement : icône, taille et couleur identiques,
nom accessible, et le badge « invité » quitte slate-100/slate-500
(4,34:1, sous le seuil) pour slate-200/slate-600 (6,15:1). Les deux
écrans montrent la même chose ; ils la montrent enfin de la même façon.

Enfin la dernière lecture publique qui ramenait trop :
getUpcomingPublishedEvents chargeait la ligne d'inscription entière —
nom, e-mail, téléphone, jeton d'annulation — pour l'accueil et
/rejoindre, qui n'en lisent que `.length`. Elle ne charge plus que
l'identifiant. Aucun effet visible, et la fuite devient impossible par
construction plutôt que par discipline.

Ce qui n'est PAS fait, volontairement : les événements lisent toujours
`signup.phone` brut, sans le repli vers la fiche d'adhérent que les
réunions obtiennent par telephonePresent(). Un membre connecté qui
s'inscrit sans retaper son numéro apparaît donc sans téléphone. Le
corriger demande de joindre associationMember dans getEventWithDetails,
qui a cinq appelants — dont l'outil MCP get_event, qui relaie l'objet
entier et sortirait le numéro privé d'un adhérent. C'est un autre
changement, avec sa propre revue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014SfQYBU4xXTeSEHKhHQXdD
@flocom
flocom force-pushed the benevoles-coordonnees-lisibles branch from 3247505 to 8223860 Compare September 19, 2026 07:41
@flocom
flocom merged commit ed259b4 into main Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants