feat(web): unified reader with reading settings, faster lists, a11y

- New Reader (desktop + mobile): loads full content on open, prefetches
  the next article, reading progress and time, "Aa" settings (light /
  sepia / dark / auto theme, serif / sans / Atkinson Hyperlegible font,
  size, line height, measure), Space-to-next, keyboard shortcuts, focus
  management, inert background, URL-driven (?a=) so Back closes it.
- Dashboard: cursor pagination, unread/all filter, CSS grid (no masonry),
  no re-sorting under the reader, optimistic cache patches instead of
  refetch storms, "new articles" banner, error/offline states, j/k/o/m/s/v
  shortcuts and help dialog.
- Cards: keyboard-openable, lazy <img>, memoized, touch-only swipe.
- Focus mode: framer drag (drop @use-gesture), Read button, undo restores
  unread state, arrow keys.
- WebSocket: single connection, backoff, cache patching, no console logs.
- Contrast fixes (on-nature tokens), 44px targets, lang="fr", reduced
  motion, self-hosted fonts, theme applied before first paint.
- Code splitting + vendor chunk, prompt-based PWA updates, smaller
  precache, offline cache for opened articles and images, caches cleared
  on logout; upgrade react-router (CVE), drop unused deps and assets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Antigravity AgentandClaude Opus 5.5 committed 2026-10-09 07:34:09 +02:00
1 parent 03e57e4308
commit 360f3cf012
38 files changed
+2557 -1242

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
import { useEffect, useRef } from 'react';
const GROUPS: { title: string; keys: [string, string][] }[] = [
{
title: 'Liste',
keys: [
['j / k', 'Article suivant / précédent'],
['o · Entrée', 'Ouvrir l’article'],
['m', 'Marquer lu / non lu'],
['s · f', 'Favori'],
['v', 'Ouvrir l’original'],
['/', 'Rechercher'],
['u', 'Basculer Non lus / Tous'],
['r', 'Actualiser les flux'],
],
},
{
title: 'Lecture',
keys: [
['Espace', 'Défiler, puis article suivant'],
['j · → / k · ←', 'Suivant / précédent'],
['+ / -', 'Taille du texte'],
['Échap', 'Fermer'],
],
},
];
export function ShortcutsHelp({ onClose }: { onClose: () => void }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
ref.current?.focus();
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape' || e.key === '?') {
e.stopImmediatePropagation();
onClose();
}
};
window.addEventListener('keydown', onKey, true);
return () => window.removeEventListener('keydown', onKey, true);
}, [onClose]);
return (
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-nature-dark/50 animate-fade-in" onClick={onClose}>
<div
ref={ref}
tabIndex={-1}
role="dialog"
aria-modal="true"
aria-labelledby="shortcuts-title"
className="surface-card w-full max-w-lg p-7 outline-none animate-rise"
onClick={(e) => e.stopPropagation()}
>
<h2 id="shortcuts-title" className="text-2xl font-serif italic text-nature mb-5">Raccourcis clavier</h2>
<div className="grid sm:grid-cols-2 gap-6">
{GROUPS.map((g) => (
<section key={g.title}>
<h3 className="eyebrow text-paper-muted mb-3">{g.title}</h3>
<dl className="space-y-2 text-sm">
{g.keys.map(([k, label]) => (
<div key={k} className="flex items-center justify-between gap-3">
<dt className="text-paper-muted">{label}</dt>
<dd className="kbd shrink-0">{k}</dd>
</div>
))}
</dl>
</section>
))}
</div>
<button onClick={onClose} className="btn-ghost w-full mt-6">Fermer</button>
</div>
</div>
);
}