d94d9e
feat: hoist note formatting toolbar inline into tabs row + inline Entry.tsx note editor (v1.4.56)
- time
- 2026-04-23 14:13 UTC · 6 months ago
- machine
- koh2
- parent
- fd0858
↓ peach-browser-d94d9e.tar.gzatlas at this save
files changed7 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/styles/popup.css+55 −8src/popup/views/Add.tsx+24 −12src/popup/views/Entry.tsx+42 −52src/popup/views/NoteEditor.tsx+21 −9manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "1.4.55",4+ "version": "1.4.56",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.55",4+ "version": "1.4.56",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.55",3+ "version": "1.4.56",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/popup/styles/popup.css+55 −8
@@ -1885,9 +1885,10 @@18851885 }18861886 .peach-entry-body::-webkit-scrollbar { display: none; }18871887 1888-/* Type-toggle pills (new entry only) */1888+/* Type-toggle pills + inline note toolbar */18891889 .peach-new-type-row {18901890 display: flex;1891+ align-items: center;18911892 gap: 6px;18921893 margin-bottom: 2px;18931894 }@@ -2358,22 +2359,68 @@23582359 pointer-events: none;23592360 }23602361 2361-/* Inline variant (Add.tsx) */2362+/* Inline variant (Add.tsx, Entry.tsx note) — toolbar hoisted to parent row */23622363 .peach-note-editor-inline {23632364 border: 1px solid var(--peach-t3);23642365 border-radius: 8px;23652366 overflow: hidden;2367+ display: flex;2368+ flex-direction: column;2369+ flex: 1;2370+ min-height: 0;23662371 }2367-.peach-note-editor-inline .peach-note-toolbar {2368- border-bottom: 1px solid var(--peach-t3);2369- background: var(--peach-t4);2370- padding: 5px 10px 4px;2371-}23722372 .peach-note-editor-body--inline {2373- min-height: 250px;2373+ flex: 1;2374+ min-height: 200px;23742375 font-size: 15px;23752376 padding: 10px 14px 14px;23762377 background: var(--peach-t4);2378+}2379+2380+/* Right-aligned inline toolbar sitting inside .peach-new-type-row (Add.tsx)2381+ or .peach-entry-note-toolbar-row (Entry.tsx). Reuses .peach-note-tool-btn2382+ and .peach-note-tool-divider button-level styles. */2383+.peach-note-toolbar-inline {2384+ display: flex;2385+ align-items: center;2386+ gap: 3px;2387+ margin-left: auto;2388+ padding-left: 10px;2389+ border-left: 1px solid var(--peach-t3);2390+ min-height: 22px;2391+}2392+2393+/* Entry.tsx toolbar row (no tabs, toolbar only) */2394+.peach-entry-note-toolbar-row {2395+ display: flex;2396+ align-items: center;2397+ margin: 4px 0 6px;2398+ position: relative;2399+ z-index: 1;2400+}2401+.peach-entry-note-toolbar-row .peach-note-toolbar-inline {2402+ border-left: none;2403+ padding-left: 0;2404+ margin-left: 0;2405+}2406+2407+/* Note body — fills available vertical space in Add.tsx and Entry.tsx */2408+.peach-note-body {2409+ display: flex;2410+ flex-direction: column;2411+ flex: 1;2412+ min-height: 0;2413+ margin: 4px 0 0;2414+}2415+.peach-entry-body--new .peach-note-body {2416+ margin: 4px 12px 0;2417+}2418+2419+/* Add.tsx body is a grid by default; swap to a flex column when a note is2420+ active so the note body can stretch and pin the tags row to the bottom. */2421+.peach-entry-body--new:has(.peach-note-body) {2422+ display: flex;2423+ flex-direction: column;23772424 }23782425 23792426 /* Rich content element styles (editor + display) */src/popup/views/Add.tsx+24 −12
@@ -2,7 +2,7 @@22 import type { VaultEntry } from "../../shared/types";33 import { sendMessage } from "../App";44 import { parseOtpauthURI } from "../../crypto/totp";5-import { NoteEditorInline } from "./NoteEditor";5+import { NoteEditorInline, Toolbar } from "./NoteEditor";66 import { GeneratorDrawer } from "../components/GeneratorDrawer";77 88 interface AddProps {@@ -34,7 +34,13 @@3434 3535 const isNoteEntry = () => props.entry?.type === "note";3636 const [selectedType, setSelectedType] = createSignal<"login" | "note">(isNoteEntry() ? "note" : "login");37+ const [noteEditorEl, setNoteEditorEl] = createSignal<HTMLDivElement | null>(null);3738 39+ function noteExec(cmd: string, val?: string) {40+ document.execCommand(cmd, false, val);41+ noteEditorEl()?.focus();42+ }43+3844 const canSave = () => {3945 if (!title().trim()) return false;4046 if (selectedType() === "login" && !password().trim()) return false;@@ -174,17 +180,22 @@174180 {/* ── Body — mirrors Entry.tsx field-card structure ── */}175181 <div class="peach-entry-body peach-entry-body--new">176182 177- {/* Type toggle — only for new entries without a fixed type */}178- <Show when={!props.entry}>183+ {/* Type toggle + inline note formatting toolbar */}184+ <Show when={!props.entry || selectedType() === "note"}>179185 <div class="peach-new-type-row">180- <button181- class={`peach-new-type-btn ${selectedType() === "login" ? "is-active" : ""}`}182- onClick={() => setSelectedType("login")}183- >login</button>184- <button185- class={`peach-new-type-btn ${selectedType() === "note" ? "is-active" : ""}`}186- onClick={() => setSelectedType("note")}187- >note</button>186+ <Show when={!props.entry}>187+ <button188+ class={`peach-new-type-btn ${selectedType() === "login" ? "is-active" : ""}`}189+ onClick={() => setSelectedType("login")}190+ >login</button>191+ <button192+ class={`peach-new-type-btn ${selectedType() === "note" ? "is-active" : ""}`}193+ onClick={() => setSelectedType("note")}194+ >note</button>195+ </Show>196+ <Show when={selectedType() === "note"}>197+ <Toolbar exec={noteExec} class="peach-note-toolbar-inline" />198+ </Show>188199 </div>189200 </Show>190201 @@ -315,10 +326,11 @@315326 316327 {/* ── Note content ── */}317328 <Show when={selectedType() === "note"}>318- <div style="margin:4px 12px 0">329+ <div class="peach-note-body">319330 <NoteEditorInline320331 initialContent={notes()}321332 onContentChange={setNotes}333+ onReady={(el) => setNoteEditorEl(el)}322334 />323335 </div>324336 </Show>src/popup/views/Entry.tsx+42 −52
@@ -1,12 +1,11 @@11 import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js";2-import { Portal } from "solid-js/web";32 import type { VaultEntry } from "../../shared/types";43 import { getDisplayDomain } from "../../shared/domain";54 import { faviconUrl } from "../../shared/favicon";65 import { generateTOTP, parseOtpauthURI } from "../../crypto/totp";76 import { sendMessage, triggerPipPulse, triggerRing } from "../App";87 import { GeneratorDrawer } from "../components/GeneratorDrawer";9-import { NoteEditor } from "./NoteEditor";8+import { NoteEditorInline, Toolbar } from "./NoteEditor";109 1110 type EditableField = "title" | "url" | "username" | "password" | "notes" | "totp" | null;1211 @@ -28,9 +27,37 @@2827 const [totpTick, setTotpTick] = createSignal(0);2928 const [confirmDelete, setConfirmDelete] = createSignal(false);3029 const [totpHot, setTotpHot] = createSignal(false);31- const [noteEditing, setNoteEditing] = createSignal(false);3230 const [faviconSrc, setFaviconSrc] = createSignal<string | null>(null);31+ const [noteEditorEl, setNoteEditorEl] = createSignal<HTMLDivElement | null>(null);32+ let lastSavedNote: string | null = null;3333 34+ function noteExec(cmd: string, val?: string) {35+ document.execCommand(cmd, false, val);36+ noteEditorEl()?.focus();37+ }38+39+ async function saveNoteInline(html: string) {40+ const entry = props.entry;41+ if (!entry) return;42+ const currentStored = entry.notes ?? "";43+ if (html === currentStored) return;44+ if (lastSavedNote !== null && html === lastSavedNote) return;45+ lastSavedNote = html;46+ setSaving(true);47+ try {48+ await sendMessage({49+ type: "UPDATE_ENTRY",50+ payload: { id: entry.id, patch: { notes: html } },51+ });52+ triggerRing();53+ await props.onFieldSaved();54+ } catch {55+ lastSavedNote = null;56+ } finally {57+ setSaving(false);58+ }59+ }60+3461 const interval = window.setInterval(() => setTotpTick((n) => n + 1), 1000);3562 onCleanup(() => window.clearInterval(interval));3663 @@ -143,25 +170,6 @@143170 setShowGenerator(false);144171 }145172 146- async function saveNote(html: string) {147- const entry = props.entry;148- if (!entry) return;149- setSaving(true);150- try {151- await sendMessage({152- type: "UPDATE_ENTRY",153- payload: { id: entry.id, patch: { notes: html } },154- });155- triggerRing();156- setNoteEditing(false);157- await props.onFieldSaved();158- } catch {159- // silent — leave editor open160- } finally {161- setSaving(false);162- }163- }164-165173 function fieldValue(entry: VaultEntry, field: Exclude<EditableField, null>): string {166174 switch (field) {167175 case "title":@@ -459,27 +467,19 @@459467 </div>460468 </Show>461469 462- {/* ── Note content ─────────────────────────────────── */}470+ {/* ── Note toolbar + inline editor ─────────────────── */}463471 <Show when={entry().type === "note"}>464- <section class="peach-fields" style="position:relative;z-index:1;">465- <Show466- when={entry().notes}467- fallback={468- <div469- class="peach-note-content peach-inline-clickable"470- style="cursor:text;"471- onClick={() => setNoteEditing(true)}472- >473- <span style="color:var(--peach-t2);font-style:italic;">click to add notes</span>474- </div>475- }476- >477- <div478- class="peach-note-content peach-inline-clickable"479- innerHTML={entry().notes}480- onClick={() => setNoteEditing(true)}481- />482- </Show>472+ <div class="peach-entry-note-toolbar-row">473+ <Toolbar exec={noteExec} class="peach-note-toolbar-inline" />474+ </div>475+ <section class="peach-fields peach-note-body" style="position:relative;z-index:1;">476+ <NoteEditorInline477+ entryKey={entry().id}478+ initialContent={entry().notes ?? ""}479+ onReady={(el) => { setNoteEditorEl(el); lastSavedNote = null; }}480+ onBlur={saveNoteInline}481+ autoFocus={false}482+ />483483 </section>484484 </Show>485485 @@ -748,16 +748,6 @@748748 </Show>749749 </div>750750 751- <Show when={noteEditing()}>752- <Portal mount={document.getElementById("hd-shell") ?? document.body}>753- <NoteEditor754- initialContent={props.entry?.notes ?? ""}755- saving={saving()}756- onSave={saveNote}757- onCancel={() => setNoteEditing(false)}758- />759- </Portal>760- </Show>761751 </>762752 );763753 }src/popup/views/NoteEditor.tsx+21 −9
@@ -1,4 +1,4 @@1-import { onMount } from "solid-js";1+import { createEffect, on, onMount } from "solid-js";22 33 export function toHtml(raw: string): string {44 if (/<\/?[a-z]/i.test(raw)) return raw;@@ -45,16 +45,16 @@4545 </svg>4646 );4747 48-type ExecFn = (cmd: string, val?: string) => void;48+export type ExecFn = (cmd: string, val?: string) => void;4949 50-function Toolbar(props: { exec: ExecFn }) {50+export function Toolbar(props: { exec: ExecFn; class?: string }) {5151 const e = props.exec;5252 function toggleHeading() {5353 const val = document.queryCommandValue("formatBlock").toLowerCase();5454 e("formatBlock", val === "h2" ? "p" : "h2");5555 }5656 return (57- <div class="peach-note-toolbar">57+ <div class={props.class ?? "peach-note-toolbar"}>5858 <button class="peach-note-tool-btn" onMouseDown={(ev) => { ev.preventDefault(); e("bold"); }} title="Bold (⌘B)"><BoldIcon /></button>5959 <button class="peach-note-tool-btn" onMouseDown={(ev) => { ev.preventDefault(); e("italic"); }} title="Italic (⌘I)"><ItalicIcon /></button>6060 <button class="peach-note-tool-btn" onMouseDown={(ev) => { ev.preventDefault(); e("underline"); }} title="Underline (⌘U)"><UnderlineIcon /></button>@@ -133,11 +133,16 @@133133 );134134 }135135 136-/* ── Inline editor (Add.tsx) ── */136+/* ── Inline editor (Add.tsx, Entry.tsx note view) ── */137137 138138 interface NoteEditorInlineProps {139139 initialContent: string;140- onContentChange: (html: string) => void;140+ onContentChange?: (html: string) => void;141+ onBlur?: (html: string) => void;142+ onReady?: (el: HTMLDivElement) => void;143+ autoFocus?: boolean;144+ /** When this key changes, the editor re-loads initialContent (e.g., switching between vault entries in-place). */145+ entryKey?: string;141146 }142147 143148 export function NoteEditorInline(props: NoteEditorInlineProps) {@@ -146,9 +151,16 @@146151 onMount(() => {147152 if (!editorRef) return;148153 editorRef.innerHTML = toHtml(props.initialContent);149- editorRef.focus();154+ if (props.autoFocus !== false) editorRef.focus();155+ props.onReady?.(editorRef);150156 });151157 158+ createEffect(on(() => props.entryKey, (key, prevKey) => {159+ if (prevKey === undefined || key === prevKey || !editorRef) return;160+ editorRef.innerHTML = toHtml(props.initialContent);161+ props.onReady?.(editorRef);162+ }));163+152164 function exec(cmd: string, value?: string) {153165 document.execCommand(cmd, false, value);154166 editorRef?.focus();@@ -163,12 +175,12 @@163175 164176 return (165177 <div class="peach-note-editor-inline">166- <Toolbar exec={exec} />167178 <div ref={editorRef}168179 class="peach-note-editor-body peach-note-editor-body--inline"169180 contenteditable="true"170181 onKeyDown={handleKeyDown}171- onInput={() => props.onContentChange(editorRef?.innerHTML ?? "")}182+ onInput={() => props.onContentChange?.(editorRef?.innerHTML ?? "")}183+ onBlur={() => props.onBlur?.(editorRef?.innerHTML ?? "")}172184 aria-multiline="true"173185 aria-label="Note content"174186 />