diff --git a/desktop/src/features/onboarding/ui/BackupStep.tsx b/desktop/src/features/onboarding/ui/BackupStep.tsx index 54c8f45cd..54ce78626 100644 --- a/desktop/src/features/onboarding/ui/BackupStep.tsx +++ b/desktop/src/features/onboarding/ui/BackupStep.tsx @@ -1,4 +1,5 @@ import { ArrowLeft, Check, Copy, Eye, EyeOff, Info } from "lucide-react"; +import { motion, type Transition, useReducedMotion } from "motion/react"; import * as React from "react"; import { getNsec } from "@/shared/api/tauriIdentity"; @@ -35,9 +36,62 @@ let introPlayed = false; const REVEAL_ANIMATION_CLASS = "animate-in fade-in duration-700 motion-reduce:animate-none"; -/** Quicker fade for switching between the chooser and a backup method. */ -const VIEW_ANIMATION_CLASS = - "animate-in fade-in duration-300 motion-reduce:animate-none"; +/** + * Shared-element id for the nsec card → shell box morph between the + * chooser and the download view. Same id on both sides lets Motion run the + * scale-down/translate-up FLIP with an automatic crossfade. + */ +const NSEC_SHELL_LAYOUT_ID = "nsec-shell-box"; + +/** Morph tween — same curve as the onboarding line-slide keyframes. */ +const SHELL_MORPH_TRANSITION: Transition = { + type: "tween", + duration: 0.55, + ease: [0.22, 1, 0.36, 1], +}; + +/** + * One half of the "[ ]" pair that clamps around the shell box once it + * settles — the visual is the user working on the secure shell that + * surrounds their key. Brackets close inward as they fade in. + */ +function ShellBracket({ + reduceMotion, + side, + visible, +}: { + reduceMotion: boolean; + side: "left" | "right"; + visible: boolean; +}) { + return ( + + ); +} /** * The chooser offers both backup methods: copying happens in place, while the @@ -84,6 +138,9 @@ type BackupStepProps = { export function BackupStep({ direction, onBack, onNext }: BackupStepProps) { const [created, setCreated] = React.useState(introPlayed); const [view, setView] = React.useState("choose"); + // True once the card → shell-box morph has finished; gates the brackets. + const [shellSettled, setShellSettled] = React.useState(false); + const reduceMotion = useReducedMotion() ?? false; const [copyState, setCopyState] = React.useState< "idle" | "copying" | "copied" >("idle"); @@ -219,81 +276,88 @@ export function BackupStep({ direction, onBack, onNext }: BackupStepProps) { > {view === "choose" ? (
- -
-
+ + +
+
+

+ {isRevealed && nsec ? nsec : maskedKey} +

+
+
+ + + + + + + Copy your key and save it somewhere safe — a password + manager is a great place for it. + + +
+
+ {copyError ? (

- {isRevealed && nsec ? nsec : maskedKey} + Could not retrieve your private key: {copyError}. You can + continue and find it later in Settings > Profile > + Identity.

-
-
- - - - - - - Copy your key and save it somewhere safe — a password - manager is a great place for it. - - -
-
- {copyError ? ( -

- Could not retrieve your private key: {copyError}. You can - continue and find it later in Settings > Profile > - Identity. -

- ) : null} -
+ ) : null} + +

@@ -304,21 +368,53 @@ export function BackupStep({ direction, onBack, onNext }: BackupStepProps) {

) : ( -
- -
-
-

Download your key

-

- Your key is encrypted with your password before it - downloads. Keep the file private — you need both it and - the password to restore your identity. -

+
+
+ + setShellSettled(true)} + transition={ + reduceMotion ? { duration: 0 } : SHELL_MORPH_TRANSITION + } + /> + +
+ + +
+
+

Download your key

+

+ Your key is encrypted with your password before it + downloads. Keep the file private — you need both it and + the password to restore your identity. +

+
+
- -
- - setView("choose")} /> + + { + setShellSettled(false); + setView("choose"); + }} + /> +
)}