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
3 files manifest.firefox.json+1 −1manifest.json+1 −1src/popup/views/Entry.tsx+147 −6
manifest.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": {

view file

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",

view file

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>

view file