import { createSignal, createMemo, createEffect, For, Show, onMount } from "solid-js"; import type { VaultEntry } from "../../shared/types"; import { sendMessage } from "../App"; import { parseOtpauthURI } from "../../crypto/totp"; import { NoteEditorInline, Toolbar } from "./NoteEditor"; import { GeneratorDrawer } from "../components/GeneratorDrawer"; interface AddProps { entry?: VaultEntry; entries?: VaultEntry[]; autoOpenGenerator?: boolean; onBack: () => void; onSave: (entry?: VaultEntry) => void | Promise; } export function AddView(props: AddProps) { const [title, setTitle] = createSignal(props.entry?.title ?? ""); const [username, setUsername] = createSignal(props.entry?.username ?? ""); const [password, setPassword] = createSignal(props.entry?.password ?? ""); const [url, setUrl] = createSignal(props.entry?.url ?? ""); const [totp, setTotp] = createSignal(props.entry?.totp ?? ""); const [notes, setNotes] = createSignal(props.entry?.notes ?? ""); const [saving, setSaving] = createSignal(false); const [error, setError] = createSignal(""); const [passwordStrength, setPasswordStrength] = createSignal(null); const [zxcvbnFn, setZxcvbnFn] = createSignal<((pwd: string) => { score: number }) | null>(null); const [tags, setTags] = createSignal(props.entry?.tags ?? []); const [tagInput, setTagInput] = createSignal(""); const [showTagSuggestions, setShowTagSuggestions] = createSignal(false); const [showGenerator, setShowGenerator] = createSignal(props.autoOpenGenerator ?? false); const [showPassword, setShowPassword] = createSignal(false); const [showTotp, setShowTotp] = createSignal(!!(props.entry?.totp)); const [showNotes, setShowNotes] = createSignal(!!(props.entry?.notes)); const isNoteEntry = () => props.entry?.type === "note"; const [selectedType, setSelectedType] = createSignal<"login" | "note">(isNoteEntry() ? "note" : "login"); const [noteEditorEl, setNoteEditorEl] = createSignal(null); function noteExec(cmd: string, val?: string) { document.execCommand(cmd, false, val); noteEditorEl()?.focus(); } const canSave = () => { if (!title().trim()) return false; if (selectedType() === "login" && !password().trim()) return false; return true; }; const allTags = createMemo(() => { const tagSet = new Set(); for (const entry of props.entries ?? []) { for (const tag of entry.tags ?? []) tagSet.add(tag); } return Array.from(tagSet).sort(); }); const tagSuggestions = createMemo(() => { const input = tagInput().toLowerCase().trim(); if (!input) return []; return allTags().filter(t => t.toLowerCase().includes(input) && !tags().includes(t) ).slice(0, 5); }); function addTag(tag: string) { const n = tag.toLowerCase().trim(); if (!n || n.length > 32 || tags().includes(n) || tags().length >= 10) return; setTags([...tags(), n]); } function removeTag(tag: string) { setTags(tags().filter(t => t !== tag)); } function handleTagKeyDown(e: KeyboardEvent) { if (e.key === "Enter" || e.key === ",") { e.preventDefault(); if (tagInput()) { addTag(tagInput()); setTagInput(""); setShowTagSuggestions(false); } } else if (e.key === "Backspace" && !tagInput() && tags().length > 0) { removeTag(tags()[tags().length - 1]); } } // Strength check createEffect(async () => { const pwd = password(); if (pwd.length < 4) { setPasswordStrength(null); return; } try { if (!zxcvbnFn()) { const mod = await import("zxcvbn"); setZxcvbnFn(() => mod.default); } const fn = zxcvbnFn(); if (fn) setPasswordStrength(fn(pwd).score); } catch { setPasswordStrength(pwd.length >= 16 ? 3 : pwd.length >= 12 ? 2 : 1); } }); onMount(() => { const el = document.querySelector(".peach-new-title-input") as HTMLInputElement | null; if (el) el.focus(); }); function getNormalizedTotp(): string | undefined { const value = totp().trim(); if (!value) return undefined; if (value.startsWith("otpauth://")) { const parsed = parseOtpauthURI(value); return parsed?.secret ?? value; } return value; } async function handleSave() { if (!canSave()) { setError(selectedType() === "note" ? "title is required" : "title and password are required"); return; } setSaving(true); setError(""); const payload: Record = { type: selectedType(), title: title().trim(), notes: notes().trim() || undefined, tags: tags().length > 0 ? tags() : undefined, }; if (selectedType() === "login") { payload.username = username().trim(); payload.password = password(); payload.url = url().trim(); payload.totp = getNormalizedTotp(); } if (props.entry) { const res = await sendMessage({ type: "UPDATE_ENTRY", payload: { id: props.entry.id, patch: payload } }); const result = res as { ok: boolean; error?: string }; if (!result.ok) { setError(result.error ?? "could not update entry"); setSaving(false); return; } await props.onSave({ ...props.entry, ...payload } as VaultEntry); return; } const res = await sendMessage({ type: "ADD_ENTRY", payload }); const result = res as { ok: boolean; data?: VaultEntry; error?: string }; if (!result.ok) { setError(result.error ?? "could not save entry"); setSaving(false); return; } await props.onSave(result.data); } const strengthLabel = () => { const s = passwordStrength(); if (s === null) return null; return ["weak", "weak", "fair", "good", "strong"][s] as string; }; return (
{props.entry ? "edit entry" : "new entry"}
{/* ── Body — mirrors Entry.tsx field-card structure ── */}
{/* Type toggle + inline note formatting toolbar */}
{/* Title */}
setTitle((e.currentTarget as HTMLInputElement).value)} onKeyDown={(e) => { if (e.key === "Enter") void handleSave(); }} />
{/* ── Login fields ── */}
setUrl((e.currentTarget as HTMLInputElement).value)} />
{/* Username */}
username
setUsername((e.currentTarget as HTMLInputElement).value)} />
{/* Password */}
password
setPassword((e.currentTarget as HTMLInputElement).value)} />
{/* Strength */}
{strengthLabel()}
{/* Generator toggle */} { setPassword(pw); setShowGenerator(false); }} />
{/* 2FA — optional */}
2fa secret
setTotp((e.currentTarget as HTMLInputElement).value)} />
{/* ── Note content ── */}
setNoteEditorEl(el)} />
{/* ── Login notes ── */}
notes