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
↓ peach-browser-1cc45518.tar.gzatlas at this save
files changed6 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 −110manifest.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": {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",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": {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}`}>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 --------------------------------------------------------------- */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-};