// ============================================================================
// detail.jsx — Pane détails d'une entrée mot de passe.
// TOTP calculés dynamiquement via CPCrypto.computeTotp (RFC 6238).
// ============================================================================
const { useState, useEffect, useRef } = React;
// useCopy + Field exportés globaux (réutilisés par wifi.jsx).
const useCopy = () => {
const [copied, setCopied] = useState(null);
const copy = (id, text) => {
try { navigator.clipboard.writeText(text); } catch (e) {}
setCopied(id);
setTimeout(() => setCopied(null), 1500);
};
return [copied, copy];
};
window.useCopy = useCopy;
const TotpRing = ({ remaining, period, size = 28 }) => {
const r = (size - 4) / 2;
const c = 2 * Math.PI * r;
const pct = remaining / period;
const danger = remaining <= 5;
return (
);
};
const AccountBlock = ({ account, copied, onCopy, isPrimary }) => {
const [show, setShow] = useState(false);
return (
{account.label}
{isPrimary && principal}
onCopy(`u-${account.id}`, account.username)}
/>
onCopy(`p-${account.id}`, account.password)}
right={
}
/>
{account.created && Créé le {account.created}
}
);
};
// TOTP : calcul live via WebCrypto. Re-render chaque seconde.
const TotpBlock = ({ totp, copied, onCopy }) => {
const [code, setCode] = useState('------');
const [remaining, setRemaining] = useState(totp.period || 30);
const period = totp.period || 30;
useEffect(() => {
let cancelled = false;
const recompute = async () => {
try {
const c = await CPCrypto.computeTotp(totp.secret, period);
if (!cancelled) {
setCode(c);
setRemaining(CPCrypto.totpRemaining(period));
}
} catch {
if (!cancelled) setCode('------');
}
};
recompute();
const id = setInterval(recompute, 1000);
return () => { cancelled = true; clearInterval(id); };
}, [totp.secret, period]);
const fmt = code && code.length === 6 ? code.slice(0, 3) + ' ' + code.slice(3) : code;
return (
{totp.label}
{fmt.slice(0, 3)}
{fmt.slice(4)}
);
};
const Field = ({ label, value, mono, copied, onCopy, right }) => (
);
const SecurityCard = ({ entry }) => {
const score = entry.strength === 'strong' ? 92 : entry.strength === 'medium' ? 58 : 24;
const colors = { strong: '#10B981', medium: '#F59E0B', weak: '#EF4444' };
const labels = {
strong: 'Mot de passe robuste',
medium: 'Force moyenne',
weak: 'Mot de passe faible',
};
const sub = {
strong: 'Long, unique, difficile à deviner.',
medium: 'Pourrait être plus long ou plus aléatoire.',
weak: 'Trop court ou réutilisé. Régénérez-le.',
};
return (
Sécurité
{labels[entry.strength] || 'Force inconnue'}
{sub[entry.strength] || ''}
);
};
// Petit menu déroulant pour le bouton "..." du hero.
const MoreMenu = ({ favorite, onToggleFavorite, onDelete }) => {
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
if (!open) return;
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, [open]);
return (
{open && (
)}
);
};
const Detail = ({ entry, onEncryptionInfo, onEdit, onDelete, onToggleFavorite }) => {
const [copied, copy] = useCopy();
if (!entry) {
return (
Sélectionnez une entrée
Cliquez sur un mot de passe pour afficher ses détails.
);
}
return (
{onEdit && (
)}
{(onDelete || onToggleFavorite) && (
)}
Comptes
{entry.accounts.length}
{onEdit && (
)}
{entry.accounts.map((a, i) => (
))}
Codes 2FA
{entry.totp.length}
{onEdit && (
)}
{entry.totp.length === 0 ? (
Aucun code 2FA configuré
Renforcez la sécurité de ce compte avec une vérification en deux étapes.
{onEdit &&
}
) : (
{entry.totp.map(t => (
))}
)}
{entry.notes && (
)}
);
};
window.Detail = Detail;
window.Field = Field;
window.MoreMenu = MoreMenu;