4e3d50
feat: full-popup note editor with rich text toolbar (v1.4.53)
- time
- 2026-04-23 07:32 UTC · 6 months ago
- machine
- koh2
- parent
- 772de5
↓ peach-browser-4e3d50.tar.gzatlas at this save
files changed6 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/styles/popup.css+159 −4src/popup/views/Entry.tsx+38 −19src/popup/views/NoteEditor.tsx+112 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "1.4.52",4+ "version": "1.4.53",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": "1.4.52",4+ "version": "1.4.53",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "1.4.52",3+ "version": "1.4.53",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/popup/styles/popup.css+159 −4
@@ -457,15 +457,14 @@457457 background 400ms ease;458458 }459459 460-/* Note content */460+/* Note content (display) */461461 .peach-note-content {462462 border-top: 1px solid var(--peach-t3);463463 padding-top: 10px;464- font-family: "IBM Plex Mono", "SFMono-Regular", Menlo, monospace;465- font-size: 11px;464+ font-family: "Cormorant Garamond", "Times New Roman", serif;465+ font-size: 15px;466466 line-height: 1.75;467467 color: var(--peach-t1);468- white-space: pre-wrap;469468 }470469 471470 /* ── Inline editing ─────────────────────────────────────────────────── */@@ -2231,6 +2230,162 @@22312230 gap: 8px;22322231 font-size: 11px;22332232 color: rgba(40, 180, 80, 0.9);2233+}2234+2235+/* ── Note editor overlay ── */2236+.peach-note-editor {2237+ position: fixed;2238+ inset: 0;2239+ z-index: 200;2240+ display: flex;2241+ flex-direction: column;2242+ background:2243+ radial-gradient(120% 80% at 0% 0%, rgba(217,104,48,0.07) 0%, transparent 65%),2244+ var(--peach-bg);2245+ animation: overlay-in 180ms cubic-bezier(0.4, 0, 0.2, 1) both;2246+}2247+2248+.peach-note-editor-header {2249+ display: flex;2250+ align-items: center;2251+ gap: 10px;2252+ padding: 14px 16px 10px;2253+ border-bottom: 1px solid var(--peach-t3);2254+ flex-shrink: 0;2255+}2256+2257+.peach-note-editor-back {2258+ background: transparent;2259+ border: none;2260+ cursor: pointer;2261+ color: var(--peach-t2);2262+ padding: 4px 6px;2263+ border-radius: 6px;2264+ display: flex;2265+ align-items: center;2266+ transition: color 120ms ease, background 120ms ease;2267+ flex-shrink: 0;2268+}2269+.peach-note-editor-back:hover { color: var(--peach-t0); background: var(--peach-t4); }2270+2271+.peach-note-editor-label {2272+ flex: 1;2273+ font-family: "Cormorant Garamond", "Times New Roman", serif;2274+ font-style: italic;2275+ font-size: 15px;2276+ color: var(--peach-t1);2277+ letter-spacing: 0.02em;2278+}2279+2280+.peach-note-editor-save-btn {2281+ background: transparent;2282+ border: 1px solid rgba(217, 104, 48, 0.4);2283+ border-radius: 8px;2284+ color: var(--peach-accent);2285+ font-family: "IBM Plex Mono", monospace;2286+ font-size: 10px;2287+ letter-spacing: 0.06em;2288+ text-transform: uppercase;2289+ padding: 3px 10px;2290+ cursor: pointer;2291+ transition: background 120ms ease, border-color 120ms ease;2292+}2293+.peach-note-editor-save-btn:hover:not(:disabled) {2294+ background: rgba(217, 104, 48, 0.1);2295+ border-color: rgba(217, 104, 48, 0.65);2296+}2297+.peach-note-editor-save-btn:disabled { opacity: 0.4; cursor: not-allowed; }2298+2299+.peach-note-toolbar {2300+ display: flex;2301+ align-items: center;2302+ gap: 3px;2303+ padding: 6px 14px 5px;2304+ border-bottom: 1px solid var(--peach-t3);2305+ flex-shrink: 0;2306+}2307+2308+.peach-note-tool-btn {2309+ background: transparent;2310+ border: 1px solid transparent;2311+ border-radius: 5px;2312+ color: var(--peach-t2);2313+ font-family: "IBM Plex Mono", monospace;2314+ font-size: 10px;2315+ letter-spacing: 0.04em;2316+ padding: 2px 8px;2317+ cursor: pointer;2318+ line-height: 1.6;2319+ transition: background 100ms ease, color 100ms ease, border-color 100ms ease;2320+}2321+.peach-note-tool-btn:hover {2322+ background: var(--peach-t4);2323+ color: var(--peach-t0);2324+ border-color: var(--peach-t3);2325+}2326+2327+.peach-note-tool-bold {2328+ font-weight: 700;2329+ font-family: Inter, system-ui, sans-serif;2330+ font-size: 11px;2331+}2332+.peach-note-tool-italic {2333+ font-style: italic;2334+ font-family: "Cormorant Garamond", serif;2335+ font-size: 14px;2336+ padding: 1px 7px;2337+}2338+2339+.peach-note-tool-divider {2340+ width: 1px;2341+ height: 14px;2342+ background: var(--peach-t3);2343+ margin: 0 4px;2344+ flex-shrink: 0;2345+}2346+2347+.peach-note-editor-body {2348+ flex: 1;2349+ overflow-y: auto;2350+ padding: 16px 20px 20px;2351+ outline: none;2352+ font-family: "Cormorant Garamond", "Times New Roman", serif;2353+ font-size: 16px;2354+ line-height: 1.75;2355+ color: var(--peach-t0);2356+ scrollbar-width: none;2357+ word-break: break-word;2358+}2359+.peach-note-editor-body:empty::before {2360+ content: "write your note…";2361+ color: var(--peach-t3);2362+ font-style: italic;2363+ pointer-events: none;2364+}2365+2366+/* font size elements from execCommand */2367+.peach-note-editor-body font[size="1"],2368+.peach-note-content font[size="1"] { font-size: 11px; line-height: 1.6; }2369+.peach-note-editor-body font[size="2"],2370+.peach-note-content font[size="2"] { font-size: 13px; }2371+.peach-note-editor-body font[size="3"],2372+.peach-note-content font[size="3"] { font-size: 16px; }2373+.peach-note-editor-body font[size="4"],2374+.peach-note-content font[size="4"] { font-size: 20px; }2375+.peach-note-editor-body font[size="5"],2376+.peach-note-content font[size="5"] { font-size: 26px; line-height: 1.4; }2377+2378+/* bold / italic inside the editor and note display */2379+.peach-note-editor-body b, .peach-note-editor-body strong,2380+.peach-note-content b, .peach-note-content strong { font-weight: 700; }2381+.peach-note-editor-body em, .peach-note-editor-body i,2382+.peach-note-content em, .peach-note-content i { font-style: italic; }2383+2384+/* note display — rendered HTML, no pre-wrap */2385+.peach-note-content {2386+ overflow: auto;2387+ max-height: 340px;2388+ word-break: break-word;22342389 }22352390 22362391 /* ── Note textarea ── */src/popup/views/Entry.tsx+38 −19
@@ -5,6 +5,7 @@55 import { generateTOTP, parseOtpauthURI } from "../../crypto/totp";66 import { sendMessage, triggerPipPulse, triggerRing } from "../App";77 import { GeneratorDrawer } from "../components/GeneratorDrawer";8+import { NoteEditor } from "./NoteEditor";89 910 type EditableField = "title" | "url" | "username" | "password" | "notes" | "totp" | null;1011 @@ -26,6 +27,7 @@2627 const [totpTick, setTotpTick] = createSignal(0);2728 const [confirmDelete, setConfirmDelete] = createSignal(false);2829 const [totpHot, setTotpHot] = createSignal(false);30+ const [noteEditing, setNoteEditing] = createSignal(false);2931 const [faviconSrc, setFaviconSrc] = createSignal<string | null>(null);3032 3133 const interval = window.setInterval(() => setTotpTick((n) => n + 1), 1000);@@ -140,6 +142,25 @@140142 setShowGenerator(false);141143 }142144 145+ async function saveNote(html: string) {146+ const entry = props.entry;147+ if (!entry) return;148+ setSaving(true);149+ try {150+ await sendMessage({151+ type: "UPDATE_ENTRY",152+ payload: { id: entry.id, patch: { notes: html } },153+ });154+ triggerRing();155+ setNoteEditing(false);156+ await props.onFieldSaved();157+ } catch {158+ // silent — leave editor open159+ } finally {160+ setSaving(false);161+ }162+ }163+143164 function fieldValue(entry: VaultEntry, field: Exclude<EditableField, null>): string {144165 switch (field) {145166 case "title":@@ -441,33 +462,22 @@441462 <Show when={entry().type === "note"}>442463 <section class="peach-fields" style="position:relative;z-index:1;">443464 <Show444- when={editingField() === "notes"}465+ when={entry().notes}445466 fallback={446467 <div447468 class="peach-note-content peach-inline-clickable"448- style="display:block;white-space:pre-wrap;overflow:auto;max-height:340px;cursor:text;"449- title="Click to edit"450- onClick={() => startEdit("notes", entry().notes ?? "")}469+ style="cursor:text;"470+ onClick={() => setNoteEditing(true)}451471 >452- {entry().notes || <span style="color:var(--t2);font-style:italic;">click to add notes</span>}472+ <span style="color:var(--peach-t2);font-style:italic;">click to add notes</span>453473 </div>454474 }455475 >456476 <div457- class="peach-inline-field-wrap"458- onFocusOut={(event) => handleEditorFocusOut("notes", event)}459- >460- <textarea461- class="peach-inline-textarea"462- value={draft()}463- onInput={(e) => setDraft(e.currentTarget.value)}464- onKeyDown={(e) => { if (e.key === "Escape") { e.preventDefault(); cancelEdit(); } }}465- ref={(el) => requestAnimationFrame(() => el?.focus())}466- rows={6}467- disabled={saving()}468- />469- <InlineSaveCancelRow onSave={saveField} onCancel={cancelEdit} saving={saving()} />470- </div>477+ class="peach-note-content peach-inline-clickable"478+ innerHTML={entry().notes}479+ onClick={() => setNoteEditing(true)}480+ />471481 </Show>472482 </section>473483 </Show>@@ -736,6 +746,15 @@736746 )}737747 </Show>738748 </div>749+750+ <Show when={noteEditing()}>751+ <NoteEditor752+ initialContent={props.entry?.notes ?? ""}753+ saving={saving()}754+ onSave={saveNote}755+ onCancel={() => setNoteEditing(false)}756+ />757+ </Show>739758 </>740759 );741760 }src/popup/views/NoteEditor.tsx+112 −0
@@ -0,0 +1,112 @@1+import { onMount } from "solid-js";2+3+interface NoteEditorProps {4+ initialContent: string;5+ saving: boolean;6+ onSave: (html: string) => Promise<void>;7+ onCancel: () => void;8+}9+10+function toHtml(raw: string): string {11+ if (/<\/?[a-z]/i.test(raw)) return raw;12+ return raw13+ .replace(/&/g, "&")14+ .replace(/</g, "<")15+ .replace(/>/g, ">")16+ .replace(/\n/g, "<br>");17+}18+19+export function NoteEditor(props: NoteEditorProps) {20+ let editorRef: HTMLDivElement | undefined;21+22+ onMount(() => {23+ if (!editorRef) return;24+ editorRef.innerHTML = toHtml(props.initialContent);25+ editorRef.focus();26+ const sel = window.getSelection();27+ if (sel) {28+ const range = document.createRange();29+ range.selectNodeContents(editorRef);30+ range.collapse(false);31+ sel.removeAllRanges();32+ sel.addRange(range);33+ }34+ });35+36+ function exec(cmd: string, value?: string) {37+ document.execCommand(cmd, false, value);38+ editorRef?.focus();39+ }40+41+ function handleKeyDown(e: KeyboardEvent) {42+ const mod = e.metaKey || e.ctrlKey;43+ if (mod && e.key === "b") { e.preventDefault(); exec("bold"); return; }44+ if (mod && e.key === "i") { e.preventDefault(); exec("italic"); return; }45+ if (e.key === "Escape") { e.preventDefault(); props.onCancel(); }46+ }47+48+ return (49+ <div class="peach-note-editor">50+ <div class="peach-note-editor-header">51+ <button52+ class="peach-note-editor-back"53+ onClick={props.onCancel}54+ disabled={props.saving}55+ aria-label="Cancel"56+ >57+ <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">58+ <path d="M6 1L1 6L6 11" />59+ </svg>60+ </button>61+ <span class="peach-note-editor-label">note</span>62+ <button63+ class="peach-note-editor-save-btn"64+ onClick={() => void props.onSave(editorRef?.innerHTML ?? "")}65+ disabled={props.saving}66+ >67+ {props.saving ? "saving…" : "save"}68+ </button>69+ </div>70+71+ <div class="peach-note-toolbar">72+ <button73+ class="peach-note-tool-btn peach-note-tool-bold"74+ onMouseDown={(e) => { e.preventDefault(); exec("bold"); }}75+ aria-label="Bold"76+ title="Bold (⌘B)"77+ >B</button>78+ <button79+ class="peach-note-tool-btn peach-note-tool-italic"80+ onMouseDown={(e) => { e.preventDefault(); exec("italic"); }}81+ aria-label="Italic"82+ title="Italic (⌘I)"83+ >I</button>84+ <div class="peach-note-tool-divider" />85+ <button86+ class="peach-note-tool-btn"87+ onMouseDown={(e) => { e.preventDefault(); exec("fontSize", "1"); }}88+ title="Small"89+ >sm</button>90+ <button91+ class="peach-note-tool-btn"92+ onMouseDown={(e) => { e.preventDefault(); exec("fontSize", "3"); }}93+ title="Normal"94+ >md</button>95+ <button96+ class="peach-note-tool-btn"97+ onMouseDown={(e) => { e.preventDefault(); exec("fontSize", "5"); }}98+ title="Large"99+ >lg</button>100+ </div>101+102+ <div103+ ref={editorRef}104+ class="peach-note-editor-body"105+ contenteditable="true"106+ onKeyDown={handleKeyDown}107+ aria-multiline="true"108+ aria-label="Note content"109+ />110+ </div>111+ );112+}