6ea3c83640b8
split identity into username + email blocks; +add button label and click handler match field semantics; legacy email-shape username migrates to entry.email when adding a separate username (v2.33.2)
- time
- 2026-05-07 01:00 UTC · 5 months ago
- machine
- koh2
- parent
- 0e1121ba9dd4
↓ peach-browser-6ea3c836.tar.gzatlas at this save
files changed3 files
manifest.firefox.json+1 −1manifest.json+1 −1src/popup/views/Entry.tsx+147 −6manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.33.1",4+ "version": "2.33.2",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.33.1",4+ "version": "2.33.2",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",src/popup/views/Entry.tsx+147 −6
@@ -10,7 +10,7 @@1010 import { GeneratorDrawer } from "../components/GeneratorDrawer";1111 import { NoteEditorInline, Toolbar } from "./NoteEditor";1212 13-type EditableField = "title" | "url" | "username" | "password" | "notes" | "totp" | null;13+type EditableField = "title" | "url" | "username" | "email" | "password" | "notes" | "totp" | null;1414 1515 interface EntryProps {1616 entry: VaultEntry | null;@@ -261,6 +261,69 @@261261 setShowGenerator(false);262262 }263263 264+ // Switch from one identity slot to the other while editing.265+ // If we're in the username slot with an email-shaped draft and no separate266+ // email yet, migrate that value into entry.email so the now-empty username267+ // slot can hold a plain username — this is the legacy-data fix-up path.268+ async function addOtherIdentity(target: "username" | "email") {269+ const entry = props.entry;270+ if (!entry) return;271+ const current = editingField();272+ const value = draft().trim();273+ if (current === "username" && target === "email") {274+ // Commit current username draft (if changed), then open email editor.275+ if (value !== (entry.username ?? "")) {276+ setSaving(true);277+ try {278+ await sendMessage({279+ type: "UPDATE_ENTRY",280+ payload: { id: entry.id, patch: { username: value } },281+ });282+ await props.onFieldSaved();283+ } finally {284+ setSaving(false);285+ }286+ }287+ startEdit("email", "");288+ return;289+ }290+ if (current === "username" && target === "username") {291+ // Already editing username — only meaningful when the saved username292+ // is email-shaped. Migrate it to entry.email and reset the slot.293+ if (value.includes("@") && !entry.email) {294+ setSaving(true);295+ try {296+ await sendMessage({297+ type: "UPDATE_ENTRY",298+ payload: { id: entry.id, patch: { email: value, username: "" } },299+ });300+ await props.onFieldSaved();301+ } finally {302+ setSaving(false);303+ }304+ }305+ startEdit("username", "");306+ return;307+ }308+ if (current === "email" && target === "username") {309+ // Commit current email draft (if changed), then open username editor.310+ if (value !== (entry.email ?? "")) {311+ setSaving(true);312+ try {313+ await sendMessage({314+ type: "UPDATE_ENTRY",315+ payload: { id: entry.id, patch: { email: value } },316+ });317+ await props.onFieldSaved();318+ } finally {319+ setSaving(false);320+ }321+ }322+ startEdit("username", "");323+ return;324+ }325+ }326+264327 function fieldValue(entry: VaultEntry, field: Exclude<EditableField, null>): string {265328 switch (field) {266329 case "title":@@ -269,6 +332,8 @@269332 return entry.url ?? "";270333 case "username":271334 return entry.username ?? "";335+ case "email":336+ return entry.email ?? "";272337 case "password":273338 return entry.password ?? "";274339 case "notes":@@ -769,10 +834,12 @@769834 <Show when={entry().type !== "note" && entry().type !== "card" && !isPasskeyOnly(entry())}>770835 <section class="peach-fields" style="position:relative;z-index:1;">771836 772- {/* Username — shown when set, or as the only identity field when neither is set */}837+ {/* Username — primary identity slot. Auto-labels as "email"838+ when the stored value is email-shaped and no separate839+ email field exists (legacy data path). */}773840 <Show when={entry().username || (!entry().username && !entry().email) || editingField() === "username"}>774841 <div class="peach-field-card" classList={{ "is-editing": editingField() === "username" }}>775- <span class="peach-label">{entry().username?.includes("@") ? "email" : "username"}</span>842+ <span class="peach-label">{entry().username?.includes("@") && !entry().email ? "email" : "username"}</span>776843 <Show777844 when={editingField() === "username"}778845 fallback={@@ -810,14 +877,88 @@810877 autocomplete="off"811878 />812879 <InlineSaveCancelRow onSave={saveField} onCancel={cancelEdit} saving={saving()} />813- <Show when={!entry().email}>880+ {/* +add the OTHER identity. If the slot is acting as881+ email (value has @, no separate email), offer882+ "+ add username" — clicking migrates the email883+ into entry.email. Otherwise offer "+ add email". */}884+ <Show when={!entry().email || !(entry().username && !entry().username?.includes("@"))}>814885 <button815886 class="peach-add-field-btn"816887 type="button"817888 onMouseDown={(e) => e.preventDefault()}818- onClick={() => startEdit("email", "")}889+ onClick={() => {890+ const isEmailShape = draft().includes("@") || (entry().username?.includes("@") ?? false);891+ const hasSeparateEmail = !!entry().email;892+ if (isEmailShape && !hasSeparateEmail) {893+ void addOtherIdentity("username");894+ } else {895+ void addOtherIdentity("email");896+ }897+ }}819898 >820- {entry().username ? "+ add email" : "+ add username"}899+ {(() => {900+ const isEmailShape = draft().includes("@") || (entry().username?.includes("@") ?? false);901+ const hasSeparateEmail = !!entry().email;902+ return (isEmailShape && !hasSeparateEmail) || hasSeparateEmail ? "+ add username" : "+ add email";903+ })()}904+ </button>905+ </Show>906+ </div>907+ </Show>908+ </div>909+ </Show>910+911+ {/* Email — secondary identity slot. Visible whenever a912+ separate email field is set, or while it is being edited. */}913+ <Show when={entry().email || editingField() === "email"}>914+ <div class="peach-field-card" classList={{ "is-editing": editingField() === "email" }}>915+ <span class="peach-label">email</span>916+ <Show917+ when={editingField() === "email"}918+ fallback={919+ <div class="peach-field-value-row">920+ <span921+ class="peach-field-value peach-inline-clickable"922+ title="Click to edit"923+ onClick={() => startEdit("email", entry().email ?? "")}924+ >925+ {entry().email}926+ </span>927+ <Show when={entry().email}>928+ <button929+ class={`peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn peach-copy-btn ${copied() === "email" ? "is-copied" : ""}`}930+ onClick={(e) => void copyField(entry().email!, "email", e.currentTarget as HTMLButtonElement)}931+ aria-label="Copy email"932+ >933+ <CopyGlyph /><span class="peach-copy-sheen" />934+ </button>935+ </Show>936+ </div>937+ }938+ >939+ <div940+ class="peach-inline-field-wrap"941+ onFocusOut={(event) => handleEditorFocusOut("email", event)}942+ >943+ <input944+ class="peach-inline-input"945+ value={draft()}946+ onInput={(e) => setDraft(e.currentTarget.value)}947+ onKeyDown={onEditKeyDown}948+ ref={autoFocus}949+ disabled={saving()}950+ autocomplete="email"951+ type="email"952+ />953+ <InlineSaveCancelRow onSave={saveField} onCancel={cancelEdit} saving={saving()} />954+ <Show when={!entry().username || entry().username?.includes("@")}>955+ <button956+ class="peach-add-field-btn"957+ type="button"958+ onMouseDown={(e) => e.preventDefault()}959+ onClick={() => void addOtherIdentity("username")}960+ >961+ + add username821962 </button>822963 </Show>823964 </div>