diff --git a/scripts/generate-og.mjs b/scripts/generate-og.mjs index 3b883d5..8965134 100644 --- a/scripts/generate-og.mjs +++ b/scripts/generate-og.mjs @@ -20,7 +20,7 @@ const fonts = [ const CAT_COLOR = { Dev: '#3b82f6', PDF: '#ef4444', Image: '#22c55e', Files: '#eab308', Draw: '#a855f7', - Media: '#ec4899', Network: '#06b6d4', Maps: '#10b981', Playground: '#f97316', + Media: '#ec4899', Network: '#06b6d4', Maps: '#10b981', Legacy: '#6366f1', Playground: '#f97316', }; // Parse the registry without importing TS (aliases + lucide). id, name, category, summary. diff --git a/src/islands/legacy/LegacyLetter.tsx b/src/islands/legacy/LegacyLetter.tsx new file mode 100644 index 0000000..97dffe0 --- /dev/null +++ b/src/islands/legacy/LegacyLetter.tsx @@ -0,0 +1,291 @@ +import { useState } from 'react'; +import { Plus, Trash2, Lock, Users, Upload, Download, Eye, EyeOff, ShieldAlert, Printer, KeyRound } from 'lucide-react'; +import { Button } from '@/components/ui/Button'; +import { Alert } from '@/components/ui/Alert'; +import { Dropzone } from '@/components/ui/Dropzone'; +import { CopyButton } from '@/components/ui/CopyButton'; +import { downloadService } from '@/services/download'; +import { + createVault, openWithPassword, openWithShares, vaultCapabilities, + VAULT_EXT, type Account, type LegacyContent, +} from '@/tools/legacy/vault.lib'; + +type Mode = 'create' | 'open' | null; +const input = 'w-full border-2 border-border bg-muted px-3 py-2 text-sm outline-none focus:shadow-brutal-sm'; +const emptyAccount = (): Account => ({ service: '', username: '', password: '', url: '', notes: '' }); + +export default function LegacyLetter() { + const [mode, setMode] = useState(null); + return ( +
+ {mode === null && ( +
+

+ Write a private letter — passwords, accounts, and final words — that your family can open only + when the time comes. It is encrypted on your device; nothing is ever uploaded. +

+
+ + +
+
+ )} + {mode === 'create' && setMode(null)} />} + {mode === 'open' && setMode(null)} />} +
+ ); +} + +function CreateView({ onBack }: { onBack: () => void }) { + const [message, setMessage] = useState(''); + const [accounts, setAccounts] = useState([emptyAccount()]); + const [password, setPassword] = useState(''); + const [confirm, setConfirm] = useState(''); + const [useShares, setUseShares] = useState(false); + const [n, setN] = useState(5); + const [k, setK] = useState(3); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(''); + const [result, setResult] = useState<{ file: string; shares: string[] } | null>(null); + + const setAccount = (i: number, patch: Partial) => + setAccounts(a => a.map((x, j) => (j === i ? { ...x, ...patch } : x))); + + const generate = async () => { + setError(''); + const usePw = password.length > 0; + if (!usePw && !useShares) { setError('Choose at least one way to unlock: a password or family shares.'); return; } + if (usePw && password !== confirm) { setError('The passwords do not match.'); return; } + if (usePw && password.length < 8) { setError('Use a password of at least 8 characters.'); return; } + if (useShares && (k < 2 || n < k || n > 20)) { setError('Check the family-share numbers (threshold ≥ 2, and total ≥ threshold).'); return; } + const content: LegacyContent = { + message, + accounts: accounts.filter(a => a.service.trim() || a.username?.trim() || a.password?.trim()), + createdAt: new Date().toISOString().slice(0, 10), + }; + setBusy(true); + try { + const res = await createVault(content, { password: usePw ? password : undefined, shares: useShares ? { n, k } : undefined }); + setResult(res); + } catch (e) { + setError(e instanceof Error ? e.message : 'Could not create the letter.'); + } finally { + setBusy(false); + } + }; + + if (result) { + return ( +
+ + Your encrypted letter is ready. Download it and keep it safe. + + + {result.shares.length > 0 && ( +
+

Family shares — hand out {k} of these {result.shares.length} are needed

+

Give each share to a different trusted person (a relative — and consider giving one to your lawyer/executor). No single person can open the letter alone; any {k} together can.

+ {result.shares.map((s, i) => ( +
+ {s} + +
+ ))} +
+ )} + +
+

Keep it recoverable

+

+ This letter can be opened only with its password{result.shares.length > 0 ? ` or ${k} family shares` : ''}. + If those are lost, the contents are gone forever — by design, so no one else can read them. Store the + file and the way to unlock it separately. +

+
+ +
+ ); + } + + return ( +
+ + +
+ +