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
↓ peach-browser-7e9b7808.tar.gzatlas at this save
files changed3 files
src/popup/styles/popup.css+175 −1src/popup/views/Export.tsx+7 −7src/popup/views/Lock.tsx+108 −23src/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 */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 }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 }