7e9b7808076a

rewrite restore picker as onboarding-style stack: PeachScript / Mayan / CSV plus snapshot footer; QR→Mayan in Export

time
2026-04-29 14:57 UTC · 6 months ago
machine
koh2
parent
0a80d4f72fe2
3 files src/popup/styles/popup.css+175 −1src/popup/views/Export.tsx+7 −7src/popup/views/Lock.tsx+108 −23
src/popup/styles/popup.css+175 −1
@@ -3510,13 +3510,187 @@35103510   border-top: 1px solid rgba(240, 232, 216, 0.06);35113511 }35123512 3513-/* Restore picker panel — method option cards */3513+/* Restore picker panel — method option cards (legacy; kept for any callers3514+   that still mount the old shape). The v2.12.0 restore picker uses the3515+   .peach-restore-stack glass cards below instead. */35143516 .peach-lock-panel--restore {35153517   gap: 10px;35163518 }35173519 .peach-lock-option--history {35183520   border-color: rgba(240, 232, 216, 0.06);35193521   opacity: 0.85;3522+}3523+3524+/* ── Restore picker (v2.12.0) ─────────────────────────────────────────────3525+   Three onboarding-style glass cards (PeachScript / Mayan code / CSV) that3526+   replace the flat option list. Visually echoes .peach-onboarding-card so3527+   the restore surface reads as part of the onboarding family. */3528+.peach-restore-screen {3529+  /* The three cards already have their own padding and spacing — no extra3530+     panel wrapper, so the screen just needs a comfortable bottom margin. */3531+  padding-bottom: 14px;3532+}3533+3534+.peach-restore-stack {3535+  position: relative;3536+  z-index: 1;3537+  display: flex;3538+  flex-direction: column;3539+  gap: 9px;3540+  margin-top: 4px;3541+}3542+3543+.peach-restore-card {3544+  position: relative;3545+  width: 100%;3546+  display: flex;3547+  flex-direction: column;3548+  gap: 6px;3549+  padding: 14px 16px 13px;3550+  border-radius: 14px;3551+  border: 1px solid var(--peach-t3);3552+  background:3553+    linear-gradient(180deg, rgba(240, 232, 216, 0.035), rgba(240, 232, 216, 0.012));3554+  backdrop-filter: blur(3px);3555+  -webkit-backdrop-filter: blur(3px);3556+  box-shadow:3557+    inset 0 1px 0 rgba(240, 232, 216, 0.04),3558+    0 8px 18px rgba(0, 0, 0, 0.18);3559+  color: var(--peach-t0);3560+  text-align: left;3561+  cursor: pointer;3562+  transition:3563+    border-color 160ms ease,3564+    transform 160ms ease,3565+    box-shadow 160ms ease,3566+    background 160ms ease;3567+  animation: peach-card-in 240ms cubic-bezier(0.16, 1, 0.3, 1) both;3568+}3569+3570+.peach-restore-card:hover {3571+  border-color: color-mix(in srgb, var(--peach-accent) 32%, transparent);3572+  background:3573+    linear-gradient(180deg, rgba(240, 232, 216, 0.05), rgba(240, 232, 216, 0.02));3574+  transform: translateY(-1px);3575+  box-shadow:3576+    inset 0 1px 0 rgba(240, 232, 216, 0.05),3577+    0 12px 24px rgba(0, 0, 0, 0.22);3578+}3579+3580+.peach-restore-card:focus-visible {3581+  outline: none;3582+  border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);3583+  box-shadow:3584+    inset 0 1px 0 rgba(240, 232, 216, 0.05),3585+    0 0 0 2px color-mix(in srgb, var(--peach-accent) 24%, transparent);3586+}3587+3588+/* The Mayan card is interactive but routes to a "decoder coming soon"3589+   stub — we still want the hover affordance, just slightly toned down. */3590+.peach-restore-card:has(.peach-restore-card-soon) {3591+  opacity: 0.78;3592+}3593+.peach-restore-card:has(.peach-restore-card-soon):hover {3594+  opacity: 0.92;3595+}3596+3597+/* Used by MayanComingSoonView for the explanatory card — non-interactive. */3598+.peach-restore-card--static {3599+  cursor: default;3600+}3601+.peach-restore-card--static:hover {3602+  border-color: var(--peach-t3);3603+  transform: none;3604+  background:3605+    linear-gradient(180deg, rgba(240, 232, 216, 0.035), rgba(240, 232, 216, 0.012));3606+  box-shadow:3607+    inset 0 1px 0 rgba(240, 232, 216, 0.04),3608+    0 8px 18px rgba(0, 0, 0, 0.18);3609+}3610+3611+.peach-restore-card-headline {3612+  margin: 0;3613+  font-family: var(--font-display);3614+  font-style: italic;3615+  font-weight: 400;3616+  font-size: 19px;3617+  line-height: 1.05;3618+  color: var(--peach-t0);3619+}3620+3621+.peach-restore-card-divider {3622+  margin: 1px 0 3px;3623+  border: 0;3624+  border-top: 1px solid var(--peach-t3);3625+  width: 100%;3626+}3627+3628+.peach-restore-card-body {3629+  margin: 0;3630+  font-size: 12px;3631+  line-height: 1.55;3632+  color: var(--peach-t1);3633+}3634+3635+.peach-restore-card-body code {3636+  font-family: "IBM Plex Mono", monospace;3637+  font-size: 11px;3638+  padding: 1px 5px;3639+  border-radius: 4px;3640+  background: rgba(240, 232, 216, 0.06);3641+  color: var(--peach-t0);3642+}3643+3644+.peach-restore-card-meta {3645+  margin: 2px 0 0;3646+  font-family: "IBM Plex Mono", monospace;3647+  font-size: 9px;3648+  letter-spacing: 0.14em;3649+  text-transform: uppercase;3650+  color: var(--peach-t2);3651+}3652+3653+.peach-restore-card-soon {3654+  position: absolute;3655+  top: 12px;3656+  right: 12px;3657+  display: inline-flex;3658+  align-items: center;3659+  padding: 2px 7px;3660+  border-radius: 999px;3661+  border: 1px solid color-mix(in srgb, var(--peach-accent) 26%, transparent);3662+  background: color-mix(in srgb, var(--peach-accent) 10%, transparent);3663+  color: var(--peach-t0);3664+  font-family: "IBM Plex Mono", monospace;3665+  font-size: 8px;3666+  letter-spacing: 0.18em;3667+  text-transform: uppercase;3668+}3669+3670+.peach-restore-footer {3671+  position: relative;3672+  z-index: 1;3673+  margin-top: 6px;3674+  padding-top: 10px;3675+  border-top: 1px solid rgba(240, 232, 216, 0.06);3676+  display: flex;3677+  justify-content: center;3678+}3679+3680+.peach-restore-footer-link {3681+  background: none;3682+  border: none;3683+  padding: 4px 0;3684+  cursor: pointer;3685+  font-family: "IBM Plex Mono", monospace;3686+  font-size: 10px;3687+  letter-spacing: 0.06em;3688+  color: var(--peach-t2);3689+  transition: color 150ms ease;3690+}3691+3692+.peach-restore-footer-link:hover {3693+  color: var(--peach-t0);35203694 }35213695 35223696 /* Snapshot restore panel */

view file

src/popup/views/Export.tsx+7 −7
@@ -69,14 +69,14 @@6969         <div class="peach-settings-group peach-export-card">7070           <p class="peach-settings-label">print backup</p>7171           <p class="peach-export-card-copy">72-            Save a QR code you can print and store. Scan it with Peach on any72+            Save a Mayan code you can print and store. Scan it with Peach on any7373             device to restore.7474           </p>7575           <button7676             class="peach-btn peach-btn--subtle peach-btn--sm"7777             onClick={() => props.onPick("qr")}7878           >79-            Save QR code →79+            Save Mayan code →8080           </button>8181         </div>8282 @@ -306,7 +306,7 @@306306       const env = await assembleEnvelope(pass());307307       if (env.bytes.length > QR_MAX_BYTES) {308308         setError(309-          `This vault is too large for a QR backup (${env.bytes.length} bytes). Use PeachScript instead.`309+          `This vault is too large for a Mayan code backup (${env.bytes.length} bytes). Use PeachScript instead.`310310         );311311         return;312312       }@@ -330,7 +330,7 @@330330     if (!url) return;331331     const a = document.createElement("a");332332     a.href = url;333-    a.download = `peach-qr-backup-${dateStamp()}.png`;333+    a.download = `peach-mayan-backup-${dateStamp()}.png`;334334     document.body.appendChild(a);335335     a.click();336336     document.body.removeChild(a);@@ -346,8 +346,8 @@346346             <div class="peach-settings-group peach-export-card peach-export-card--primary">347347               <p class="peach-settings-label">backup password</p>348348               <p class="peach-export-card-copy">349-                Set a password to lock this QR backup. You'll need it to350-                restore — without it the printed code is unreadable.349+                Set a password to lock this Mayan code backup. You'll need it350+                to restore — without it the printed code is unreadable.351351               </p>352352               <div class="peach-settings-stack peach-export-password-stack">353353                 <input@@ -383,7 +383,7 @@383383                 onClick={() => void handleEncode()}384384                 disabled={working() || !passReady()}385385               >386-                {working() ? "encoding…" : "Generate QR code"}386+                {working() ? "encoding…" : "Generate Mayan code"}387387               </button>388388             </div>389389           }

view file

src/popup/views/Lock.tsx+108 −23
@@ -25,8 +25,9 @@2525   onBackToOnboarding?: () => void;2626 }2727 28-// "restore" = picker, "snapshots" = vault history, "peachscript" / "file" = detail flows29-type RestoreSurface = "restore" | "snapshots" | "peachscript" | "file" | null;28+// "restore" = picker, "snapshots" = vault history, "peachscript" / "file" = detail flows,29+// "mayan" = Mayan-code import (decoder not shipped yet — picker exposes a "soon" card).30+type RestoreSurface = "restore" | "snapshots" | "peachscript" | "file" | "mayan" | null;3031 3132 export function LockView(props: LockProps) {3233   const [password, setPassword] = createSignal("");@@ -140,11 +141,17 @@140141         <RestorePickerView141142           onBack={() => setRestoreSurface(null)}142143           onPickPeachScript={() => setRestoreSurface("peachscript")}143-          onPickFile={() => setRestoreSurface("file")}144+          onPickMayan={() => setRestoreSurface("mayan")}145+          onPickCSV={() => setRestoreSurface("file")}144146           onPickSnapshots={() => setRestoreSurface("snapshots")}145147         />146148       </Match>147149 150+      {/* ── Mayan code restore (decoder coming soon) ──────────────── */}151+      <Match when={restoreSurface() === "mayan"}>152+        <MayanComingSoonView onBack={() => setRestoreSurface("restore")} />153+      </Match>154+148155       {/* ── Vault snapshot history ────────────────────────────────── */}149156       <Match when={restoreSurface() === "snapshots"}>150157         <SnapshotRestoreView@@ -395,12 +402,13 @@395402 function RestorePickerView(props: {396403   onBack: () => void;397404   onPickPeachScript: () => void;398-  onPickFile: () => void;405+  onPickMayan: () => void;406+  onPickCSV: () => void;399407   onPickSnapshots: () => void;400408 }) {401409   return (402410     <section403-      class="peach-screen peach-theme lock-screen lock-screen--vault-stage lock-screen--onboarding"411+      class="peach-screen peach-theme lock-screen lock-screen--vault-stage lock-screen--onboarding peach-restore-screen"404412       data-motion-phase="enter"405413     >406414       <header class="peach-screen-header lock-screen-header">@@ -419,30 +427,107 @@419427         </h1>420428       </div>421429 422-      <section class="peach-lock-panel peach-lock-panel--restore">423-        <p class="peach-hint" style="margin-bottom:2px;">How did you back up your data?</p>430+      <div class="peach-restore-stack" role="list">431+        <button432+          class="peach-restore-card"433+          role="listitem"434+          onClick={props.onPickPeachScript}435+        >436+          <h2 class="peach-restore-card-headline">PeachScript</h2>437+          <hr class="peach-restore-card-divider" />438+          <p class="peach-restore-card-body">439+            Paste a string of Peach symbols, or open a <code>.peach</code> file440+            you saved from another device.441+          </p>442+          <p class="peach-restore-card-meta">backup password required</p>443+        </button>424444 425-        <button class="peach-lock-option" onClick={props.onPickPeachScript}>426-          <span class="peach-lock-option-title">peachscript</span>427-          <span class="peach-lock-option-copy">428-            A string of Peach symbols you exported from another Peach device. You'll need the backup password you set at the time.429-          </span>445+        <button446+          class="peach-restore-card"447+          role="listitem"448+          onClick={props.onPickMayan}449+        >450+          <span class="peach-restore-card-soon" aria-label="coming soon">soon</span>451+          <h2 class="peach-restore-card-headline">Mayan code</h2>452+          <hr class="peach-restore-card-divider" />453+          <p class="peach-restore-card-body">454+            Restore from a printed Mayan code by uploading a photo of it.455+          </p>456+          <p class="peach-restore-card-meta">decoder shipping soon</p>430457         </button>431458 432-        <button class="peach-lock-option" onClick={props.onPickFile}>433-          <span class="peach-lock-option-title">backup file</span>434-          <span class="peach-lock-option-copy">435-            A Peach JSON export, or a file from Bitwarden, Proton Pass, 1Password, or a browser (Chrome, Safari, Edge CSV).436-          </span>459+        <button460+          class="peach-restore-card"461+          role="listitem"462+          onClick={props.onPickCSV}463+        >464+          <h2 class="peach-restore-card-headline">CSV</h2>465+          <hr class="peach-restore-card-divider" />466+          <p class="peach-restore-card-body">467+            Move in from Bitwarden, 1Password, Proton Pass, or a browser468+            (Chrome, Safari, Edge) CSV export.469+          </p>470+          <p class="peach-restore-card-meta">plain-text — open the file once and delete it</p>437471         </button>472+      </div>438473 439-        <button class="peach-lock-option peach-lock-option--history" onClick={props.onPickSnapshots}>440-          <span class="peach-lock-option-title">vault history</span>441-          <span class="peach-lock-option-copy">442-            Peach saves encrypted snapshots locally after each change. Revert to an earlier version if something went wrong.443-          </span>474+      <div class="peach-restore-footer">475+        <button476+          class="peach-restore-footer-link"477+          onClick={props.onPickSnapshots}478+        >479+          or revert to a local snapshot →444480         </button>445-      </section>481+      </div>482+    </section>483+  );484+}485+486+// ── Mayan code (decoder coming soon) ───────────────────────────────────────487+488+function MayanComingSoonView(props: { onBack: () => void }) {489+  return (490+    <section491+      class="peach-screen peach-theme lock-screen lock-screen--vault-stage lock-screen--onboarding peach-restore-screen"492+      data-motion-phase="enter"493+    >494+      <header class="peach-screen-header lock-screen-header">495+        <PeachWordmark class="peach-wordmark-wrap" />496+        <button class="peach-lock-restore-link" onClick={props.onBack}>497+          ‹ back498+        </button>499+      </header>500+501+      <div class="peach-lock-hero">502+        <span class="peach-lock-eyebrow">mayan code</span>503+        <h1 class="peach-onboarding-headline">504+          decoder505+          <br />506+          on its way.507+        </h1>508+      </div>509+510+      <div class="peach-restore-stack">511+        <div class="peach-restore-card peach-restore-card--static">512+          <h2 class="peach-restore-card-headline">Not yet — but soon.</h2>513+          <hr class="peach-restore-card-divider" />514+          <p class="peach-restore-card-body">515+            Mayan codes encode the same backup as PeachScript, but printable on516+            a single page. The phone-camera decoder is the next thing to land517+            for this format.518+          </p>519+          <p class="peach-restore-card-body">520+            Until then, restore your vault using the PeachScript that came521+            with this Mayan code, or open a <code>.peach</code> file backup.522+          </p>523+        </div>524+      </div>525+526+      <div class="peach-restore-footer">527+        <button class="peach-restore-footer-link" onClick={props.onBack}>528+          ‹ back to restore529+        </button>530+      </div>446531     </section>447532   );448533 }

view file