eb76531da401

feat: Consolidate note creation in Notes mode (v4.10.3)

time
2026-08-14 22:04 UTC · 2 months ago
machine
koh2
parent
35b94380f5ed
8 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/components/NoteToolbar.tsx+6 −0src/popup/styles/popup.css+7 −23src/popup/views/Add.tsx+4 −35src/popup/views/NoteEditor.tsx+1 −1src/popup/views/contracts.test.tsx+32 −0
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "4.10.2",4+  "version": "4.10.3",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -2,7 +2,7 @@22   "manifest_version": 3,33   "minimum_chrome_version": "116",44   "name": "Peach Password Manager",5-  "version": "4.10.2",5+  "version": "4.10.3",66   "description": "Zero-knowledge password manager",77   "action": {88     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "4.10.2",3+  "version": "4.10.3",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/popup/components/NoteToolbar.tsx+6 −0
@@ -6,6 +6,7 @@66 function applyBold(exec: ToolbarExec) { exec("bold"); }77 function applyItalic(exec: ToolbarExec) { exec("italic"); }88 function applyUnderline(exec: ToolbarExec) { exec("underline"); }9+function applyStrike(exec: ToolbarExec) { exec("strikeThrough"); }910 function applyHeading(exec: ToolbarExec) {1011   const val = document.queryCommandValue("formatBlock").toLowerCase();1112   exec("formatBlock", val === "h2" ? "p" : "h2");@@ -16,6 +17,7 @@1617 }1718 function applyBullet(exec: ToolbarExec) { exec("insertUnorderedList"); }1819 function applyNumbered(exec: ToolbarExec) { exec("insertOrderedList"); }20+function applyRule(exec: ToolbarExec) { exec("insertHorizontalRule"); }1921 2022 interface ToolbarProps {2123   exec: ToolbarExec;@@ -34,11 +36,13 @@3436           <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyBold(props.exec); }} title="Bold">B</button>3537           <button class="peach-note-toolbar-btn peach-note-toolbar-btn--italic" onMouseDown={(e) => { e.preventDefault(); applyItalic(props.exec); }} title="Italic">I</button>3638           <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyUnderline(props.exec); }} title="Underline">U</button>39+          <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyStrike(props.exec); }} title="Strikethrough">S</button>3740           <div class="peach-note-toolbar-divider" />3841           <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyHeading(props.exec); }} title="Heading">H</button>3942           <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyQuote(props.exec); }} title="Quote">"</button>4043           <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyBullet(props.exec); }} title="Bullet">•</button>4144           <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyNumbered(props.exec); }} title="Numbered">1.</button>45+          <button class="peach-note-toolbar-btn" onMouseDown={(e) => { e.preventDefault(); applyRule(props.exec); }} title="Divider">—</button>4246         </div>4347       }4448     >@@ -48,11 +52,13 @@4852           <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyBold(props.exec); }}>B</button>4953           <button class="peach-note-toolbar-btn peach-note-toolbar-btn--italic" onPointerDown={(e) => { e.preventDefault(); applyItalic(props.exec); }}>I</button>5054           <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyUnderline(props.exec); }}>U</button>55+          <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyStrike(props.exec); }}>S</button>5156           <div class="peach-note-toolbar-divider" />5257           <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyHeading(props.exec); }}>H</button>5358           <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyQuote(props.exec); }}>"</button>5459           <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyBullet(props.exec); }}>•</button>5560           <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyNumbered(props.exec); }}>1.</button>61+          <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); applyRule(props.exec); }}>—</button>5662           <div class="peach-note-toolbar-spacer" />5763           <button class="peach-note-toolbar-btn" onPointerDown={(e) => { e.preventDefault(); props.onDismissKeyboard?.(); }}>⌨</button>5864         </div>

view file

src/popup/styles/popup.css+7 −23
@@ -3189,18 +3189,6 @@31893189   padding: 10px 20px 14px;31903190 }31913191 3192-/* Full-width toolbar row inside the body (Add.tsx note type).3193-   Negative horizontal margins cancel body padding so the border spans edge-to-edge. */3194-.peach-note-toolbar-body {3195-  display: flex;3196-  align-items: center;3197-  gap: 3px;3198-  padding: 5px 20px 4px;3199-  margin: 2px -20px 0;3200-  border-bottom: 1px solid var(--peach-t3);3201-  flex-shrink: 0;3202-}3203-32043192 /* Right-aligned inline toolbar sitting inside .peach-new-type-row (Add.tsx)32053193    or .peach-entry-note-toolbar-row (Entry.tsx). Reuses .peach-note-tool-btn32063194    and .peach-note-tool-divider button-level styles. */@@ -3236,17 +3224,6 @@32363224   min-height: 0;32373225   margin: 4px 0 0;32383226 }3239-.peach-entry-body--new .peach-note-body {3240-  margin: 8px -20px 0;3241-}3242-3243-/* Add.tsx body is a grid by default; swap to a flex column when a note is3244-   active so the note body can stretch and pin the tags row to the bottom. */3245-.peach-entry-body--new:has(.peach-note-body) {3246-  display: flex;3247-  flex-direction: column;3248-}3249-32503227 /* Rich content element styles (editor + display) */32513228 .peach-note-editor-body h2,32523229 .peach-note-content h2 {@@ -3296,6 +3273,13 @@32963273 .peach-note-content b, .peach-note-content strong { font-weight: 700; }32973274 .peach-note-editor-body em, .peach-note-editor-body i,32983275 .peach-note-content em, .peach-note-content i { font-style: italic; }3276+.peach-note-content blockquote {3277+  border-left: 2px solid var(--peach-accent);3278+  margin: 12px 0;3279+  padding-left: 16px;3280+  color: var(--peach-t1);3281+  font-style: italic;3282+}32993283 33003284 /* note display — rendered HTML, no pre-wrap */33013285 .peach-note-content {

view file

src/popup/views/Add.tsx+4 −35
@@ -2,7 +2,6 @@22 import type { VaultEntry } from "../../shared/types";33 import { sendMessage } from "../App";44 import { parseOtpauthURI } from "../../crypto/totp";5-import { NoteEditorInline, Toolbar } from "./NoteEditor";65 import { GeneratorDrawer } from "../components/GeneratorDrawer";76 87 interface AddProps {@@ -81,10 +80,9 @@8180       .slice(0, 8);8281   });8382 84-  const isNoteEntry = () => props.entry?.type === "note";8583   const isCardEntry = () => props.entry?.type === "card";86-  const [selectedType, setSelectedType] = createSignal<"login" | "note" | "card">(87-    isNoteEntry() ? "note" : isCardEntry() ? "card" : "login"84+  const [selectedType, setSelectedType] = createSignal<"login" | "card">(85+    isCardEntry() ? "card" : "login"8886   );8987 9088   // Card signals@@ -96,13 +94,6 @@9694   const [cvc, setCvc] = createSignal(props.entry?.cvc ?? "");9795   const [showCvc, setShowCvc] = createSignal(false);9896   const [cardNumberFocused, setCardNumberFocused] = createSignal(false);99-  const [noteEditorEl, setNoteEditorEl] = createSignal<HTMLDivElement | null>(null);100-101-  function noteExec(cmd: string, val?: string) {102-    document.execCommand(cmd, false, val);103-    noteEditorEl()?.focus();104-  }105-10697   function getCardNetwork(num: string): string {10798     const n = num.replace(/\s/g, '');10899     if (/^4/.test(n)) return 'Visa';@@ -214,9 +205,7 @@214205   async function handleSave() {215206     if (!canSave()) {216207       setError(217-        selectedType() === "note"218-          ? "title is required"219-          : selectedType() === "card"208+        selectedType() === "card"220209           ? "cardholder name and card number are required"221210           : "title is required"222211       );@@ -314,26 +303,17 @@314303               onClick={() => setSelectedType("login")}315304             >login</button>316305             <button317-              class={`peach-new-type-btn ${selectedType() === "note" ? "is-active" : ""}`}318-              onClick={() => setSelectedType("note")}319-            >note</button>320-            <button321306               class={`peach-new-type-btn ${selectedType() === "card" ? "is-active" : ""}`}322307               onClick={() => setSelectedType("card")}323308             >card</button>324309           </div>325310         </Show>326311 327-        {/* Note formatting toolbar — own row so it never overflows */}328-        <Show when={selectedType() === "note"}>329-          <Toolbar exec={noteExec} class="peach-note-toolbar-body" />330-        </Show>331-332312         {/* Title */}333313         <div class="peach-new-title-wrap">334314           <input335315             class="peach-new-title-input"336-            placeholder={selectedType() === "note" ? "note title" : "entry name"}316+            placeholder="entry name"337317             autocomplete="off"338318             value={title()}339319             onInput={(e) => setTitle((e.currentTarget as HTMLInputElement).value)}@@ -619,17 +599,6 @@619599               </div>620600             </div>621601           </section>622-        </Show>623-624-        {/* ── Note content ── */}625-        <Show when={selectedType() === "note"}>626-          <div class="peach-note-body">627-            <NoteEditorInline628-              initialContent={notes()}629-              onContentChange={setNotes}630-              onReady={(el) => setNoteEditorEl(el)}631-            />632-          </div>633602         </Show>634603 635604         {/* ── Login notes ── */}

view file

src/popup/views/NoteEditor.tsx+1 −1
@@ -6,7 +6,7 @@66 // to it requires reviewing the attack surface of the new tag.77 const ALLOWED_NOTE_TAGS = new Set([88   "b", "i", "u", "s", "strong", "em",9-  "h2", "h3", "ul", "ol", "li", "br", "hr", "p",9+  "h2", "h3", "blockquote", "ul", "ol", "li", "br", "hr", "p",1010 ]);1111 1212 function sanitizeNoteElement(el: Element): void {

view file

src/popup/views/contracts.test.tsx+32 −0
@@ -12,6 +12,8 @@1212 import { DEFAULT_BINDINGS } from "../../shared/shortcuts";1313 import { EntryView } from "./Entry";1414 import { ImportView } from "./Import";15+import { AddView } from "./Add";16+import { NotesEditor } from "./NotesEditor";1517 import { RecoveryWordEntryView } from "./RecoveryWordEntry";1618 import { VaultView } from "./Vault";1719 import { changeSelect, flushView, inputText, pressEnter, renderView } from "./view-test-utils";@@ -268,6 +270,36 @@268270     const unlockIndex = buttons.findIndex((button) => button.textContent?.trim() === "unlock");269271     const pinIndex = buttons.findIndex((button) => button.textContent?.trim() === "use PIN");270272     expect(pinIndex).toBeGreaterThan(unlockIndex);273+  });274+275+  it("keeps standalone note creation in Notes mode", async () => {276+    const chromeApi = globalThis.chrome as unknown as {277+      tabs?: { query: (queryInfo: object, callback: (tabs: unknown[]) => void) => void };278+    };279+    chromeApi.tabs = { query: vi.fn((_queryInfo, callback) => callback([])) };280+281+    const addContainer = mount(<AddView onBack={vi.fn()} onSave={vi.fn()} />);282+    await flushView();283+284+    expect(Array.from(addContainer.querySelectorAll(".peach-new-type-btn")).map((button) => button.textContent?.trim()))285+      .toEqual(["login", "card"]);286+    expect(addContainer.querySelector(".peach-note-editor-body--inline")).toBeNull();287+  });288+289+  it("gives Notes mode the full note-formatting toolbar", async () => {290+    const container = mount(291+      <NotesEditor292+        note={null}293+        onBack={vi.fn()}294+        onSaved={vi.fn()}295+        onDelete={vi.fn()}296+      />297+    );298+    await flushView();299+300+    expect(container.querySelector('[title="Strikethrough"]')).toBeTruthy();301+    expect(container.querySelector('[title="Divider"]')).toBeTruthy();302+    expect(container.querySelector('[title="Quote"]')).toBeTruthy();271303   });272304 273305   it("Lock keeps create-vault disabled for short passwords and weak passphrases", async () => {

view file