915845b2a0ad

replace notes flying-title animation with smooth 200ms crossfade (v2.41.3)

time
2026-05-13 20:31 UTC · 5 months ago
machine
koh2
parent
cac826341309
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/styles/popup.css+9 −72src/popup/views/NotesMode.tsx+17 −131
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.41.2",4+  "version": "2.41.3",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.41.2",4+  "version": "2.41.3",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "2.41.2",3+  "version": "2.41.3",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/popup/styles/popup.css+9 −72
@@ -8163,22 +8163,25 @@81638163 .peach-notes-toc-wrapper {81648164   position: absolute;81658165   inset: 0;8166-  transition: opacity 180ms ease-out;81678166   z-index: 1;8167+  opacity: 1;8168+  pointer-events: auto;8169+  transition: opacity 200ms ease;81688170 }81698171 8170-.peach-notes-toc-wrapper.is-visible { opacity: 1; pointer-events: auto; }8171-.peach-notes-toc-wrapper.is-expanding { opacity: 0; pointer-events: none; transition: opacity 150ms ease-out; }8172-.peach-notes-toc-wrapper.is-collapsing { opacity: 1; pointer-events: none; }8172+.peach-notes-toc-wrapper.is-hidden {8173+  opacity: 0;8174+  pointer-events: none;8175+}81738176 81748177 /* Editor wrapper */81758178 .peach-notes-editor-wrapper {81768179   position: absolute;81778180   inset: 0;8181+  z-index: 2;81788182   opacity: 0;81798183   pointer-events: none;8180-  z-index: 2;8181-  transition: opacity 220ms ease-in;8184+  transition: opacity 200ms ease;81828185 }81838186 81848187 .peach-notes-editor-wrapper.is-visible {@@ -8186,24 +8189,6 @@81868189   pointer-events: auto;81878190 }81888191 8189-.peach-notes-editor-wrapper.is-entering {8190-  animation: peach-note-editor-enter 220ms 180ms ease-in both;8191-}8192-8193-.peach-notes-editor-wrapper.is-leaving {8194-  animation: peach-note-editor-leave 200ms ease-out both;8195-}8196-8197-@keyframes peach-note-editor-enter {8198-  from { opacity: 0; transform: translateY(6px); }8199-  to   { opacity: 1; transform: translateY(0); }8200-}8201-8202-@keyframes peach-note-editor-leave {8203-  from { opacity: 1; transform: translateY(0); }8204-  to   { opacity: 0; transform: translateY(6px); }8205-}8206-82078192 /* TOC inner */82088193 .peach-notes-toc {82098194   position: relative;@@ -8343,15 +8328,6 @@83438328   background: rgba(255,255,255,0.02);83448329 }83458330 8346-/* Row fade animations (expansion) */8347-.peach-note-row.is-expanding-away {8348-  transition: opacity 150ms ease, transform 150ms ease;8349-  opacity: 0;8350-}8351-8352-.peach-note-row.is-expanding-away-up { transform: translateY(-6px); }8353-.peach-note-row.is-expanding-away-down { transform: translateY(6px); }8354-83558331 .peach-note-numeral {83568332   font-family: 'Cormorant Garamond', serif;83578333   font-style: italic;@@ -8692,42 +8668,3 @@86928668 }86938669 86948670 /* Flying title — shared element during expansion/collapse */8695-.peach-note-expanding-title {8696-  position: absolute;8697-  left: 20px;8698-  right: 20px;8699-  font-family: 'Cormorant Garamond', serif;8700-  font-style: italic;8701-  font-weight: 600;8702-  font-size: 32px;8703-  color: var(--peach-t0);8704-  line-height: 1.2;8705-  white-space: nowrap;8706-  overflow: hidden;8707-  text-overflow: ellipsis;8708-  pointer-events: none;8709-  z-index: 50;8710-  transform-origin: left top;8711-  transition:8712-    top 280ms cubic-bezier(0, 0, 0, 1),8713-    transform 280ms cubic-bezier(0, 0, 0, 1);8714-  will-change: top, transform;8715-}8716-8717-/* TOC wrapper states */8718-.peach-notes-toc-wrapper.is-toc { opacity: 1; pointer-events: auto; }8719-.peach-notes-toc-wrapper.is-expanding {8720-  opacity: 0;8721-  pointer-events: none;8722-  transition: opacity 150ms ease-out;8723-}8724-.peach-notes-toc-wrapper.is-collapsing {8725-  pointer-events: none;8726-  transition: opacity 200ms 100ms ease-in;8727-  opacity: 1;8728-}8729-8730-/* Expanding target row hides its title text (flying title takes over) */8731-.peach-note-row.is-expanding-target .peach-note-title {8732-  opacity: 0;8733-}

view file

src/popup/views/NotesMode.tsx+17 −131
@@ -1,4 +1,4 @@1-import { createSignal, onCleanup, onMount, Show } from "solid-js";1+import { createSignal, onMount, Show } from "solid-js";22 import type { VaultEntry } from "../../shared/types";33 import { sendMessage } from "../../shared/bridge";44 import { NotesTOC } from "./NotesTOC";@@ -11,39 +11,16 @@1111   notes: VaultEntry[];1212 }1313 14-type Phase =15-  | "toc"          // TOC fully visible16-  | "expanding"    // rows fading, title flying up17-  | "editor"       // editor fully visible18-  | "collapsing";  // editor fading, title flying down14+type Phase = "toc" | "editor" | "collapsing";1915 20-interface ExpansionOrigin {21-  top: number;      // px from top of container22-  title: string;23-}16+const FADE_MS = 200;2417 25-// Editor title top offset (below header + spacing)26-const EDITOR_TITLE_TOP = 68;27-const EXPAND_DURATION = 320;28-const COLLAPSE_DURATION = 320;29-3018 export function NotesMode(props: NotesModeProps) {31-  let modeEl!: HTMLDivElement;32-  let flyingTitleEl: HTMLDivElement | undefined;33-  let tocWrapperEl: HTMLDivElement | undefined;34-  let editorWrapperEl: HTMLDivElement | undefined;35-3619   const [phase, setPhase] = createSignal<Phase>("toc");3720   const [activeNote, setActiveNote] = createSignal<VaultEntry | null>(null);3821   const [isNewNote, setIsNewNote] = createSignal(false);39-  const [origin, setOrigin] = createSignal<ExpansionOrigin | null>(null);40-  const [flyingTitle, setFlyingTitle] = createSignal<string>("");41-  const [flyingTop, setFlyingTop] = createSignal<number>(0);42-  const [flyingFontSize, setFlyingFontSize] = createSignal<number>(18);43-  const [flyingVisible, setFlyingVisible] = createSignal(false);4422 4523   onMount(() => {46-    // Direct-to-editor from global search4724     if (props.initialNoteId) {4825       const note = props.notes.find((n) => n.id === props.initialNoteId);4926       if (note) {@@ -57,40 +34,12 @@5734     try { navigator.vibrate?.(ms); } catch { /* unsupported */ }5835   }5936 60-  function handleNoteRowTap(note: VaultEntry, rowEl: HTMLElement) {37+  function handleNoteRowTap(note: VaultEntry, _rowEl: HTMLElement) {6138     if (phase() !== "toc") return;6239     vibrate(12);63-64-    const rect = rowEl.getBoundingClientRect();65-    const containerRect = modeEl.getBoundingClientRect();66-    const rowTop = rect.top - containerRect.top;67-    // The title text starts about 50% vertically within the row68-    const titleTop = rowTop + (rect.height - 18) / 2;69-70-    setOrigin({ top: titleTop, title: note.title || "Untitled" });71-    setFlyingTitle(note.title || "Untitled");72-    setFlyingTop(titleTop);73-    setFlyingFontSize(18);74-    setFlyingVisible(true);7540     setActiveNote(note);7641     setIsNewNote(false);77-78-    // Phase 1: show flying title at row position, fade TOC rows79-    setPhase("expanding");80-81-    // Phase 2: animate title to editor position82-    requestAnimationFrame(() => {83-      requestAnimationFrame(() => {84-        setFlyingTop(EDITOR_TITLE_TOP);85-        setFlyingFontSize(32);86-      });87-    });88-89-    // Phase 3: after animation, show editor and hide flying title90-    setTimeout(() => {91-      setPhase("editor");92-      setFlyingVisible(false);93-    }, EXPAND_DURATION);42+    requestAnimationFrame(() => setPhase("editor"));9443   }9544 9645   function handleNewNote() {@@ -98,61 +47,24 @@9847     vibrate(12);9948     setActiveNote(null);10049     setIsNewNote(true);101-    setOrigin(null);102-    // Simple expand from bottom — no flying title for new note103-    setPhase("expanding");104-    requestAnimationFrame(() => {105-      setTimeout(() => {106-        setPhase("editor");107-      }, 80);108-    });50+    requestAnimationFrame(() => setPhase("editor"));10951   }11052 11153   function handleEditorBack() {11254     if (phase() !== "editor") return;11355     vibrate(12);114-115-    const orig = origin();116-    if (orig) {117-      // Animate title back to its row118-      setFlyingTitle(activeNote()?.title || "Untitled");119-      setFlyingTop(EDITOR_TITLE_TOP);120-      setFlyingFontSize(32);121-      setFlyingVisible(true);122-      setPhase("collapsing");123-124-      requestAnimationFrame(() => {125-        requestAnimationFrame(() => {126-          setFlyingTop(orig.top);127-          setFlyingFontSize(18);128-        });129-      });130-131-      setTimeout(() => {132-        setFlyingVisible(false);133-        setActiveNote(null);134-        setIsNewNote(false);135-        setOrigin(null);136-        setPhase("toc");137-      }, COLLAPSE_DURATION);138-    } else {139-      // No animation (new note or direct open)140-      setPhase("collapsing");141-      setTimeout(() => {142-        setActiveNote(null);143-        setIsNewNote(false);144-        setPhase("toc");145-      }, 200);146-    }56+    setPhase("collapsing");57+    setTimeout(() => {58+      setActiveNote(null);59+      setIsNewNote(false);60+      setPhase("toc");61+    }, FADE_MS);14762   }14863 14964   function handleSaved(note: VaultEntry) {15065     if (!activeNote() || activeNote()!.id !== note.id) {15166       setActiveNote(note);15267     }153-    if (flyingTitle() !== note.title) {154-      setFlyingTitle(note.title || "Untitled");155-    }15668     props.onNoteListChanged();15769   }15870 @@ -163,16 +75,11 @@16375   }16476 16577   return (166-    <div ref={modeEl} class="peach-notes-mode" data-phase={phase()}>167-      {/* TOC wrapper */}78+    <div class="peach-notes-mode" data-phase={phase()}>79+      {/* TOC — fades out when editor opens */}16880       <div169-        ref={tocWrapperEl}17081         class="peach-notes-toc-wrapper"171-        classList={{172-          "is-toc": phase() === "toc",173-          "is-expanding": phase() === "expanding",174-          "is-collapsing": phase() === "collapsing",175-        }}82+        classList={{ "is-hidden": phase() === "editor" }}17683         aria-hidden={phase() !== "toc"}17784       >17885         <NotesTOC@@ -181,35 +88,14 @@18188           onOpenNote={handleNoteRowTap}18289           onNewNote={handleNewNote}18390           onRefresh={props.onNoteListChanged}184-          expandingNoteId={phase() === "expanding" || phase() === "collapsing" ? (activeNote()?.id ?? null) : null}18591         />18692       </div>18793 188-      {/* Flying title — shared element between TOC and editor during animation */}189-      <Show when={flyingVisible()}>190-        <div191-          ref={flyingTitleEl}192-          class="peach-note-expanding-title"193-          style={{194-            top: `${flyingTop()}px`,195-            transform: `scale(${flyingFontSize() / 32})`,196-          }}197-          aria-hidden="true"198-        >199-          {flyingTitle()}200-        </div>201-      </Show>202-203-      {/* Editor wrapper */}94+      {/* Editor — fades in over the TOC */}20495       <Show when={activeNote() !== null || isNewNote()}>20596         <div206-          ref={editorWrapperEl}20797           class="peach-notes-editor-wrapper"208-          classList={{209-            "is-visible": phase() === "editor",210-            "is-entering": phase() === "expanding",211-            "is-leaving": phase() === "collapsing",212-          }}98+          classList={{ "is-visible": phase() === "editor" }}21399         >214100           <NotesEditor215101             note={activeNote()}

view file