1cc4551868f2

unify Sync rest view with new Chirp surface — bird stage at idle pose, Pair-a-device CTA, paired-devices section with remove confirm; drop old idle card and ChirpHeroBirds (v3.3.2)

time
2026-05-18 13:18 UTC · 5 months ago
machine
koh2
parent
1f2ff58944c2
6 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/components/ChirpPairingFlow.tsx+2 −2src/popup/styles/popup.css+47 −1src/popup/views/ChirpSync.tsx+106 −110
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "3.3.1",4+  "version": "3.3.2",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": "3.3.1",4+  "version": "3.3.2",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": "3.3.1",3+  "version": "3.3.2",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/popup/components/ChirpPairingFlow.tsx+2 −2
@@ -398,7 +398,7 @@398398   cream: "#f7e0c8", beak: "#f5c764", beakShade: "#b87a22", legs: "#5e3a1f",399399 };400400 401-const DesktopBird: Component<{ pose: string }> = (props) => {401+export const DesktopBird: Component<{ pose: string }> = (props) => {402402   const p = FINCH;403403   return (404404     <div class={`peach-chirp-bird peach-chirp-bird-desktop peach-chirp-bird-pose-${props.pose}`}>@@ -471,7 +471,7 @@471471   );472472 };473473 474-const MobileBird: Component<{ pose: string }> = (props) => {474+export const MobileBird: Component<{ pose: string }> = (props) => {475475   const p = SWALLOW;476476   return (477477     <div class={`peach-chirp-bird peach-chirp-bird-mobile peach-chirp-bird-pose-${props.pose}`}>

view file

src/popup/styles/popup.css+47 −1
@@ -8739,15 +8739,19 @@87398739   display: flex;87408740   flex-direction: column;87418741   padding: 16px 18px 14px;8742+  height: 100%;87428743   min-height: 100%;87438744   gap: 0;87448745   background:87458746     radial-gradient(ellipse 70% 50% at 30% 0%, color-mix(in srgb, var(--peach-accent) 8%, transparent), transparent 70%),87468747     var(--peach-bg);87478748   color: var(--peach-text);8748-  overflow: hidden;8749+  overflow-y: auto;8750+  overflow-x: hidden;8751+  scrollbar-width: none;87498752   isolation: isolate;87508753 }8754+.peach-chirp-pair::-webkit-scrollbar { width: 0; height: 0; display: none; }87518755 .peach-chirp-pair::before {87528756   content: '';87538757   position: absolute;@@ -9354,6 +9358,48 @@93549358   text-align: center;93559359   max-width: 320px;93569360   margin: 0 auto;9361+}9362+9363+/* Rest state — bird stage at idle pose, primary CTA + paired list */9364+.peach-chirp-pair[data-phase="idle"] .peach-chirp-stage {9365+  /* Slightly shorter stage when at rest — no flight wave or motes to fill it */9366+  height: 160px;9367+}9368+.peach-chirp-pair-start {9369+  width: 100%;9370+  display: inline-flex;9371+  align-items: center;9372+  justify-content: center;9373+  gap: 8px;9374+  padding: 11px 14px;9375+  border-radius: 10px;9376+  background: var(--peach-accent);9377+  border: 1px solid color-mix(in srgb, var(--peach-accent) 80%, #000);9378+  color: #1a0905;9379+  font-family: inherit;9380+  font-size: 13px;9381+  font-weight: 600;9382+  letter-spacing: 0.01em;9383+  cursor: pointer;9384+  transition: filter 0.15s ease, transform 0.1s ease;9385+}9386+.peach-chirp-pair-start:hover { filter: brightness(1.08); }9387+.peach-chirp-pair-start:active { transform: translateY(1px); }9388+9389+.peach-chirp-paired-section {9390+  margin-top: 14px;9391+  display: flex;9392+  flex-direction: column;9393+  gap: 8px;9394+}9395+.peach-chirp-paired-heading {9396+  margin: 0;9397+  font-family: var(--peach-mono, ui-monospace, monospace);9398+  font-size: 10px;9399+  font-weight: 500;9400+  letter-spacing: 0.14em;9401+  text-transform: uppercase;9402+  color: var(--peach-text-muted);93579403 }93589404 93599405 /* Footer --------------------------------------------------------------- */

view file

src/popup/views/ChirpSync.tsx+106 −110
@@ -29,7 +29,7 @@2929   PairedDevice,3030   PairingProgress,3131 } from "../../shared/sync-types";32-import { ChirpPairingFlow } from "../components/ChirpPairingFlow";32+import { ChirpPairingFlow, DesktopBird, MobileBird } from "../components/ChirpPairingFlow";3333 import {3434   ChirpPairingFailure,3535   runChirpPairing,@@ -46,11 +46,11 @@4646 4747 const STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;4848 49-type Mode = "idle" | "pairing";49+type Mode = "rest" | "pairing";5050 5151 export const ChirpSyncView: Component<ChirpSyncViewProps> = (props) => {5252   const [devices, setDevices] = createSignal<PairedDevice[]>([]);53-  const [mode, setMode] = createSignal<Mode>("idle");53+  const [mode, setMode] = createSignal<Mode>("rest");5454   const [pairing, setPairing] = createSignal<PairingProgress>({ phase: "idle" });5555   const [error, setError] = createSignal<string | null>(null);5656   const [confirmUnpair, setConfirmUnpair] = createSignal<string | null>(null);@@ -155,7 +155,7 @@155155         return;156156       }157157       setError((err as Error).message);158-      setMode("idle");158+      setMode("rest");159159     }160160   }161161 @@ -217,124 +217,144 @@217217     </div>218218   );219219 220-  const idleView = () => (221-    <div class="peach-chirp-settings">222-      <header class="peach-chirp-settings-header">223-        <button class="peach-icon-btn" aria-label="Back" onClick={() => props.onBack()}>224-          <svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">225-            <path d="M11 4 L5 9 L11 14" />220+  const restView = () => (221+    <div class="peach-chirp-pair" data-phase="idle" role="region" aria-label="Sync">222+      <header class="peach-chirp-head">223+        <button class="peach-chirp-back" aria-label="Back" onClick={() => props.onBack()}>224+          <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">225+            <path d="M10 3 L5 8 L10 13" />226226           </svg>227227         </button>228-        <h1 class="peach-chirp-settings-title">Sync</h1>228+        <div class="peach-chirp-title">Sync</div>229+        <div class="peach-chirp-head-spacer" />229230       </header>230231 232+      <div class="peach-chirp-chip-row">233+        <div class="peach-chirp-chip" data-phase="idle">234+          <span class="peach-chirp-chip-dot" />235+          <span class="peach-chirp-chip-text">ready</span>236+        </div>237+      </div>238+231239       <Show when={error()}>232240         <div class="peach-chirp-settings-error" role="alert">{error()}</div>233241       </Show>234242 235-      <Show when={devices().length === 0}>236-        <div class="peach-chirp-hero">237-          <ChirpHeroBirds />238-          <h2 class="peach-chirp-hero-headline">Pair your phone with this Peach</h2>239-          <p class="peach-chirp-hero-sub">240-            Hold your phone near your laptop. Peach will chirp once,241-            inaudibly, and pair automatically. No codes, no servers.242-          </p>243+      <div class="peach-chirp-stage act-idle" aria-hidden="true">244+        <div class="peach-chirp-bird-slot peach-chirp-bird-slot-desktop">245+          <DesktopBird pose="idle" />243246         </div>244-      </Show>247+        <div class="peach-chirp-bird-slot peach-chirp-bird-slot-mobile">248+          <MobileBird pose="hidden" />249+        </div>250+      </div>245251 246-      <div class="peach-chirp-pair-cta">247-        <button class="peach-btn peach-btn-primary peach-chirp-pair-button" onClick={() => void handleStartPairing()}>248-          <svg class="peach-chirp-pair-button-icon" width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">252+      <div class="peach-chirp-copy-host">253+        <div class="peach-chirp-copy peach-chirp-copy-rest">254+          <div class="peach-chirp-headline">255+            {devices().length === 0 ? "Pair your phone with this Peach" : "Sync is ready"}256+          </div>257+          <div class="peach-chirp-sub">258+            {devices().length === 0259+              ? "Hold your phone near your laptop. Peach will chirp once, inaudibly, and pair automatically."260+              : "Pair another device, or manage the devices already connected below."}261+          </div>262+        </div>263+      </div>264+265+      <div class="peach-chirp-action">266+        <button class="peach-chirp-pair-start" onClick={() => void handleStartPairing()}>267+          <svg width="16" height="16" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">249268             <path d="M3 9 L9 3 L15 9" />250269             <path d="M3 13 L9 7 L15 13" opacity="0.5" />251270           </svg>252-          Pair a device271+          <span>{devices().length === 0 ? "Pair a device" : "Pair another device"}</span>253272         </button>254273       </div>255274 256275       <Show when={devices().length > 0}>257-        <h2 class="peach-chirp-settings-section">Paired devices</h2>258-        <ul class="peach-chirp-device-list">259-          <For each={devices()}>260-            {(d) => (261-              <li class="peach-chirp-device-row" data-stale={isStale(d) ? "true" : "false"}>262-                <DeviceTypeIcon type={d.deviceType ?? "extension"} />263-                <div class="peach-chirp-device-info">264-                  <span class="peach-chirp-device-name">{d.deviceName ?? "Unnamed device"}</span>265-                  <span class="peach-chirp-device-meta">266-                    {relativeTime(d.lastSyncedAt)}267-                    <Show when={d.trustSource === "vouched"}>268-                      <span class="peach-chirp-device-badge">Added automatically</span>269-                    </Show>270-                    <Show when={isStale(d)}>271-                      <span class="peach-chirp-device-stale">Not seen recently</span>272-                    </Show>273-                  </span>274-                </div>275-                <Show276-                  when={confirmUnpair() !== d.deviceId}277-                  fallback={278-                    <div class="peach-chirp-unpair-confirm">279-                      <button class="peach-btn peach-btn-ghost peach-btn-sm" onClick={() => setConfirmUnpair(null)}>280-                        Cancel281-                      </button>282-                      <button class="peach-btn peach-btn-destructive peach-btn-sm" onClick={() => void handleUnpair(d.deviceId)}>283-                        Remove284-                      </button>285-                    </div>286-                  }287-                >288-                  <button289-                    class="peach-icon-btn peach-chirp-device-remove"290-                    aria-label={`Unpair ${d.deviceName}`}291-                    onClick={() => setConfirmUnpair(d.deviceId)}276+        <section class="peach-chirp-paired-section">277+          <h2 class="peach-chirp-paired-heading">Paired devices</h2>278+          <ul class="peach-chirp-device-list">279+            <For each={devices()}>280+              {(d) => (281+                <li class="peach-chirp-device-row" data-stale={isStale(d) ? "true" : "false"}>282+                  <DeviceTypeIcon type={d.deviceType ?? "extension"} />283+                  <div class="peach-chirp-device-info">284+                    <span class="peach-chirp-device-name">{d.deviceName ?? "Unnamed device"}</span>285+                    <span class="peach-chirp-device-meta">286+                      {relativeTime(d.lastSyncedAt)}287+                      <Show when={d.trustSource === "vouched"}>288+                        <span class="peach-chirp-device-badge">Added automatically</span>289+                      </Show>290+                      <Show when={isStale(d)}>291+                        <span class="peach-chirp-device-stale">Not seen recently</span>292+                      </Show>293+                    </span>294+                  </div>295+                  <Show296+                    when={confirmUnpair() !== d.deviceId}297+                    fallback={298+                      <div class="peach-chirp-unpair-confirm">299+                        <button class="peach-btn peach-btn-ghost peach-btn-sm" onClick={() => setConfirmUnpair(null)}>300+                          Cancel301+                        </button>302+                        <button class="peach-btn peach-btn-destructive peach-btn-sm" onClick={() => void handleUnpair(d.deviceId)}>303+                          Remove304+                        </button>305+                      </div>306+                    }292307                   >293-                    <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">294-                      <path d="M3 3 L11 11 M11 3 L3 11" />295-                    </svg>296-                  </button>297-                </Show>298-              </li>299-            )}300-          </For>301-        </ul>308+                    <button309+                      class="peach-icon-btn peach-chirp-device-remove"310+                      aria-label={`Unpair ${d.deviceName}`}311+                      onClick={() => setConfirmUnpair(d.deviceId)}312+                    >313+                      <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">314+                        <path d="M3 3 L11 11 M11 3 L3 11" />315+                      </svg>316+                    </button>317+                  </Show>318+                </li>319+              )}320+            </For>321+          </ul>322+        </section>302323       </Show>303324 304-      <footer class="peach-chirp-settings-footer">305-        <p>306-          Chirp Sync stays entirely on your local network. No relay servers,307-          no cloud accounts — just your devices in the same room.308-        </p>325+      <footer class="peach-chirp-foot">326+        <div class="peach-chirp-foot-divider" />327+        <div class="peach-chirp-foot-text">328+          Chirp Sync stays on your local network. No relay servers, no cloud329+          accounts — just your devices in the same room.330+        </div>309331       </footer>310332     </div>311333   );312334 313335   const pairingView = () => (314-    <div class="peach-chirp-settings peach-chirp-settings--pairing">315-      <ChirpPairingFlow316-        progress={pairing()}317-        onCancel={() => setMode("idle")}318-        onRetry={() => void handleStartPairing()}319-        onDismiss={() => {320-          setMode("idle");321-          void refresh();322-        }}323-      />324-    </div>336+    <ChirpPairingFlow337+      progress={pairing()}338+      onCancel={() => setMode("rest")}339+      onRetry={() => void handleStartPairing()}340+      onDismiss={() => {341+        setMode("rest");342+        void refresh();343+      }}344+    />325345   );326346 327347   const currentMode = createMemo(() => {328348     if (!props.isPro) return "gated";329349     if (mode() === "pairing") return "pairing";330-    return "idle";350+    return "rest";331351   });332352 333353   return (334354     <Show335355       when={currentMode() === "gated"}336356       fallback={337-        <Show when={currentMode() === "pairing"} fallback={idleView()}>357+        <Show when={currentMode() === "pairing"} fallback={restView()}>338358           {pairingView()}339359         </Show>340360       }@@ -364,27 +384,3 @@364384   );365385 };366386 367-// Schematic two-bird illustration shown when no devices are paired yet.368-// The desktop bird stands ready, the mobile bird hops in from the right —369-// frozen mid-greeting. The live pairing animation drives the same SVG370-// shapes from ChirpPairingFlow once the user taps "Pair a device".371-const ChirpHeroBirds: Component = () => {372-  return (373-    <svg class="peach-chirp-hero-svg" width="240" height="120" viewBox="0 0 240 120" fill="none">374-      <ellipse cx="80" cy="78" rx="32" ry="24" class="peach-chirp-bird-body" />375-      <circle cx="98" cy="56" r="16" class="peach-chirp-bird-head" />376-      <circle cx="103" cy="54" r="2.4" class="peach-chirp-bird-eye" />377-      <path d="M 113 56 L 122 56 L 116 62 Z" class="peach-chirp-bird-beak" />378-      <path d="M 60 64 Q 50 76 60 86" class="peach-chirp-bird-wing" fill="none" stroke-width="2.4" stroke-linecap="round" />379-380-      <path d="M 130 60 Q 160 36 180 60" stroke="var(--peach-accent)" stroke-width="1.6" fill="none" stroke-linecap="round" opacity="0.45" />381-      <path d="M 130 70 Q 160 46 180 70" stroke="var(--peach-accent)" stroke-width="1.6" fill="none" stroke-linecap="round" opacity="0.25" />382-383-      <ellipse cx="184" cy="80" rx="22" ry="17" class="peach-chirp-bird-body" />384-      <circle cx="170" cy="64" r="11" class="peach-chirp-bird-head" />385-      <circle cx="166" cy="62" r="1.8" class="peach-chirp-bird-eye" />386-      <path d="M 158 64 L 151 64 L 156 69 Z" class="peach-chirp-bird-beak" />387-      <path d="M 200 72 Q 210 82 200 92" class="peach-chirp-bird-wing" fill="none" stroke-width="2.2" stroke-linecap="round" />388-    </svg>389-  );390-};

view file