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
↓ peach-browser-eb76531d.tar.gzatlas at this save
files changed8 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 −0manifest.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": {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",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",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>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 {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 ── */}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 {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 () => {