/* passwords.jsx — раздел «Пароли»: список, показать/скрыть, копировать, * генератор случайных паролей, индикатор надёжности. */ /* ----------------------------------------------------------- копирование с тостом */ function useCopied() { const [copied, setCopied] = React.useState(null); const copy = (text, key) => { copyToClipboard(text).then(() => { setCopied(key); setTimeout(() => setCopied((c) => (c === key ? null : c)), 1400); }); }; return [copied, copy]; } /* ----------------------------------------------------------- индикатор силы */ function StrengthBar({ value }) { const s = passwordStrength(value); return (
{[0, 1, 2, 3].map((i) => ( ))}
{s.label}
); } /* ----------------------------------------------------------- генератор (панель) */ function GeneratorPanel({ onUse }) { const [length, setLength] = React.useState(16); const [upper, setUpper] = React.useState(true); const [digits, setDigits] = React.useState(true); const [symbols, setSymbols] = React.useState(true); const [value, setValue] = React.useState(() => generatePassword({ length: 16 })); const regen = React.useCallback(() => { setValue(generatePassword({ length, upper, digits, symbols })); }, [length, upper, digits, symbols]); React.useEffect(() => { regen(); }, [length, upper, digits, symbols]); const [copied, copy] = useCopied(); const Toggle = ({ on, set, label }) => ( ); return (
{value}
Длина {length}
setLength(+e.target.value)} className="nb-range w-full" />
{onUse && ( )}
); } /* ----------------------------------------------------------- карточка пароля */ function PasswordCard({ item, onEdit, onToggleFav, flags = [], index = 0 }) { const settings = Store.useSettings(); const [show, setShow] = React.useState(false); const [copied, copy] = useCopied(); const initials = (item.title || "?").trim().slice(0, 1).toUpperCase(); // автоскрытие открытого пароля React.useEffect(() => { if (!show) return; const secs = settings.autoHidePw || 0; if (!secs) return; const t = setTimeout(() => setShow(false), secs * 1000); return () => clearTimeout(t); }, [show, settings.autoHidePw]); const flagLabel = { weak: "слабый", reused: "повтор", old: "давно не менялся" }; return (
{initials}

{item.title}

{item.url && ( )}
{item.login &&

{item.login}

} {flags.length > 0 && (
{flags.map((fl) => ( {flagLabel[fl]} ))}
)}
{show ? item.password : "••••••••••••"}
{item.login && ( )} {item.note && · {item.note}}
); } /* ----------------------------------------------------------- модалка пароля */ function PasswordModal({ item, open, onClose, onSaved, onDeleted }) { const empty = { title: "", login: "", password: "", url: "", note: "", favorite: false }; const [form, setForm] = React.useState(empty); const [show, setShow] = React.useState(false); const [gen, setGen] = React.useState(false); const [busy, setBusy] = React.useState(false); const [confirmDel, setConfirmDel] = React.useState(false); const isEdit = item && item.id; React.useEffect(() => { if (open) { setForm(item ? { ...empty, ...item } : empty); setShow(false); setGen(!item); setConfirmDel(false); } }, [open, item]); const set = (k, v) => setForm((f) => ({ ...f, [k]: v })); const save = async () => { if (!form.title.trim()) return; setBusy(true); if (isEdit) await api.updatePassword(item.id, form); else await api.createPassword(form); setBusy(false); onSaved && onSaved(); onClose(); }; const del = async () => { setBusy(true); await api.deletePassword(item.id); setBusy(false); onDeleted && onDeleted(); onClose(); }; return (

{isEdit ? "Изменить пароль" : "Новый пароль"}

set("title", e.target.value)} placeholder="Например, Личная почта" className="w-full rounded-xl border border-[var(--border)] bg-[var(--surface-2)] px-3.5 h-[42px] text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition" />
set("login", e.target.value)} placeholder="логин" className="w-full rounded-xl border border-[var(--border)] bg-[var(--surface-2)] px-3.5 h-[42px] text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition" /> set("url", e.target.value)} placeholder="example.com" className="w-full rounded-xl border border-[var(--border)] bg-[var(--surface-2)] px-3.5 h-[42px] text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition" />
set("password", e.target.value)} placeholder="пароль" className="mono flex-1 bg-transparent outline-none text-[14px] text-[var(--text)]" />
{form.password &&
}
{gen && { set("password", v); setShow(true); }} />} set("note", e.target.value)} placeholder="например, включена 2FA" className="w-full rounded-xl border border-[var(--border)] bg-[var(--surface-2)] px-3.5 h-[42px] text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition" />
{isEdit && (!confirmDel ? ( ) : (
))}
); } /* ----------------------------------------------------------- замок (PIN, zero-knowledge) */ const PIN_LEN = 4; function VaultGate({ onUnlocked }) { const [phase, setPhase] = React.useState("checking"); // checking | unset | locked const [stage, setStage] = React.useState("enter"); // для unset: enter | confirm const [pin, setPin] = React.useState(""); const [firstPin, setFirstPin] = React.useState(""); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(""); React.useEffect(() => { window.vault.status().then(setPhase).catch(() => setPhase("locked")); }, []); const finish = async (code) => { setBusy(true); setErr(""); try { if (phase === "unset") await window.vault.setup(code); else await window.vault.unlock(code); onUnlocked(); } catch (e) { setErr(phase === "unset" ? "Не удалось сохранить PIN." : "Неверный PIN"); setPin(""); setBusy(false); } }; const press = (d) => { if (busy) return; setErr(""); setPin((prev) => { if (prev.length >= PIN_LEN) return prev; const v = prev + d; if (v.length === PIN_LEN) { if (phase === "unset") { if (stage === "enter") { setFirstPin(v); setStage("confirm"); setTimeout(() => setPin(""), 150); } else if (v === firstPin) { finish(v); } else { setErr("PIN не совпал, начни заново"); setStage("enter"); setFirstPin(""); setTimeout(() => setPin(""), 350); } } else { finish(v); } } return v; }); }; const doReset = async () => { if (!window.confirm("Сбросить PIN? Все сохранённые пароли будут удалены безвозвратно.")) return; setBusy(true); try { await api.resetKeyInfo(); window.vault.lock(); setPhase("unset"); setStage("enter"); setFirstPin(""); setPin(""); setErr(""); } catch (e) { setErr("Не удалось сбросить."); } finally { setBusy(false); } }; if (phase === "checking") { return
; } const title = phase === "locked" ? "Введи PIN" : stage === "confirm" ? "Повтори PIN" : "Придумай PIN"; const sub = phase === "locked" ? "Короткий код для доступа к паролям на этом устройстве." : "4 цифры. Ими шифруются пароли прямо в браузере — сервер видит только шифр. Восстановить нельзя."; return (

{title}

{sub}

{Array.from({ length: PIN_LEN }).map((_, i) => ( ))}
{err &&

{err}

}
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => ( ))}
{phase === "locked" && ( )}
); } /* ----------------------------------------------------------- баннер здоровья */ function VaultHealth({ health, active, onToggle }) { if (!health) return null; const issues = new Set([...health.weak, ...health.reused, ...health.old]).size; const tone = health.score >= 80 ? "green" : health.score >= 50 ? "amber" : "red"; return ( ); } /* ----------------------------------------------------------- экран «Пароли» */ function PasswordsScreen({ createSignal }) { const [unlocked, setUnlocked] = React.useState(() => (window.vault ? window.vault.isUnlocked() : true)); const [items, setItems] = React.useState([]); const [loading, setLoading] = React.useState(true); const [q, setQ] = React.useState(""); const [editing, setEditing] = React.useState(null); const [modalOpen, setModalOpen] = React.useState(false); const [health, setHealth] = React.useState(null); const [showHealth, setShowHealth] = React.useState(false); const [blurred, setBlurred] = React.useState(false); // блюр при сворачивании приложения (безопасность) React.useEffect(() => { const onVis = () => setBlurred(document.hidden); document.addEventListener("visibilitychange", onVis); return () => document.removeEventListener("visibilitychange", onVis); }, []); const load = React.useCallback(async () => { setLoading(true); const data = await api.getPasswords({ q: q || undefined }); setItems(data); setLoading(false); api.getVaultHealth().then(setHealth); }, [q]); React.useEffect(() => { if (unlocked) { const t = setTimeout(load, 200); return () => clearTimeout(t); } }, [q, unlocked]); const firstSignal = React.useRef(createSignal); React.useEffect(() => { if (createSignal !== firstSignal.current) { setEditing(null); setModalOpen(true); } }, [createSignal]); const openNew = () => { setEditing(null); setModalOpen(true); }; const openEdit = (item) => { setEditing(item); setModalOpen(true); }; const toggleFav = async (item) => { await api.updatePassword(item.id, { favorite: !item.favorite }); load(); }; const flagsFor = (id) => { if (!health || !showHealth) return []; const f = []; if (health.weak.includes(id)) f.push("weak"); if (health.reused.includes(id)) f.push("reused"); if (health.old.includes(id)) f.push("old"); return f; }; if (!unlocked) { return (
setUnlocked(true)} />
); } return (
setShowHealth((s) => !s)} />
setQ(e.target.value)} placeholder="Поиск по названию или логину…" className="w-full h-11 rounded-xl border border-[var(--border)] bg-[var(--surface)] pl-10 pr-3 text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition placeholder:text-[var(--muted)]" />
{loading ? (
{[0, 1, 2].map((i) =>
)}
) : items.length === 0 ? ( Добавить пароль} /> ) : (
{items.map((p, i) => )}
)} setModalOpen(false)} onSaved={load} onDeleted={load} />
); } Object.assign(window, { PasswordsScreen, PasswordModal, PasswordCard, GeneratorPanel, StrengthBar });