mirror of
https://github.com/R0m1k3/FlowReader.git
synced 2026-10-11 17:28:05 +02:00
- 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>
75 lines
2.8 KiB
TypeScript
75 lines
2.8 KiB
TypeScript
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>
|
||
);
|
||
}
|