From a0d69ae83ea67d05b4051341030efcef16359d4e Mon Sep 17 00:00:00 2001 From: yasha-meursault Date: Mon, 30 Mar 2026 14:00:00 +0400 Subject: [PATCH 01/19] sidebar initial implementation --- apps/app/Models/Wizard.ts | 3 +- apps/app/components/HeaderWithMenu/index.tsx | 20 +-- .../components/Input/RoutePicker/index.tsx | 4 +- .../SecretDerivation/LoginModal/index.tsx | 18 ++- apps/app/components/Sidebar/AppSidebar.tsx | 106 +++++++++++++ apps/app/components/Sidebar/NavbarActions.tsx | 16 ++ .../components/Sidebar/SidebarMenuList.tsx | 11 ++ apps/app/components/TrainMenu/Menu.tsx | 3 +- apps/app/components/TrainMenu/MenuList.tsx | 25 +-- apps/app/components/TrainMenu/index.tsx | 36 ++--- .../components/Wallet/ConnectedWallets.tsx | 39 ++++- apps/app/components/globalFooter.tsx | 2 +- apps/app/components/layout.tsx | 34 ++-- apps/app/components/navbar.tsx | 12 +- apps/app/components/sendFeedback.tsx | 2 +- apps/app/components/shadcn/sidebar.tsx | 150 ++++++++++++++++++ apps/app/components/themeWrapper.tsx | 76 ++++----- apps/app/hooks/useMenuNavigation.ts | 68 ++++++++ 18 files changed, 491 insertions(+), 134 deletions(-) create mode 100644 apps/app/components/Sidebar/AppSidebar.tsx create mode 100644 apps/app/components/Sidebar/NavbarActions.tsx create mode 100644 apps/app/components/Sidebar/SidebarMenuList.tsx create mode 100644 apps/app/components/shadcn/sidebar.tsx create mode 100644 apps/app/hooks/useMenuNavigation.ts diff --git a/apps/app/Models/Wizard.ts b/apps/app/Models/Wizard.ts index 28a5b9e0..a602f6cb 100644 --- a/apps/app/Models/Wizard.ts +++ b/apps/app/Models/Wizard.ts @@ -59,7 +59,8 @@ export enum MenuStep { TransactionDetails = "Transaction Details", RPCConfiguration = "RPC Configuration", NetworkRPCEdit = "Network RPC Edit", - RecoverSwap = "Recover Swap" + RecoverSwap = "Recover Swap", + SuggestFeature = "Suggest a Feature" } export type Steps = AuthStep | SwapWithdrawalStep | SwapCreateStep | MenuStep diff --git a/apps/app/components/HeaderWithMenu/index.tsx b/apps/app/components/HeaderWithMenu/index.tsx index 189350f2..627d3cb5 100644 --- a/apps/app/components/HeaderWithMenu/index.tsx +++ b/apps/app/components/HeaderWithMenu/index.tsx @@ -36,18 +36,14 @@ function HeaderWithMenu({ goBack }: { goBack: (() => void) | undefined | null }) } -
- { - isMobile - ? <> - - - - : null - } - - -
+ {isMobile && ( +
+ + + + +
+ )} ) } diff --git a/apps/app/components/Input/RoutePicker/index.tsx b/apps/app/components/Input/RoutePicker/index.tsx index 7d7db1af..3ecf5ea6 100644 --- a/apps/app/components/Input/RoutePicker/index.tsx +++ b/apps/app/components/Input/RoutePicker/index.tsx @@ -11,6 +11,7 @@ import useWallet from "@/hooks/useWallet"; import useSuggestionsLimit from "@/hooks/useSuggestionsLimit"; import Balance from "@/components/Input/Amount/Balance"; import PickerWalletConnect from "./PickerWalletConnect"; +import useWindowDimensions from "@/hooks/useWindowDimensions"; const RoutePicker: FC<{ direction: SwapDirection, className?: string }> = ({ direction, className }) => { const { @@ -19,6 +20,7 @@ const RoutePicker: FC<{ direction: SwapDirection, className?: string }> = ({ dir } = useFormikContext(); const [searchQuery, setSearchQuery] = useState("") const { wallets } = useWallet() + const { isMobile } = useWindowDimensions() const { suggestionsLimit } = useSuggestionsLimit({ hasWallet: wallets.length > 0 }); @@ -44,7 +46,7 @@ const RoutePicker: FC<{ direction: SwapDirection, className?: string }> = ({ dir } + header={isMobile ? : undefined} > {({ closeModal }) => ( -
- {icon} -
-
-

{title}

-

{subtitle}

+
+
+
+ {icon} +
+
+

{title}

+

{subtitle}

+
-
+
{error && onRetry && ( +

{currentStepName as string}

+
+ )} + + +
+ + + + + + + + + {selectedNetwork ? ( + + ) : ( +
Loading...
+ )} +
+ + + + + + + + + +
+
+
+ + ) +} + +export default AppSidebar diff --git a/apps/app/components/Sidebar/NavbarActions.tsx b/apps/app/components/Sidebar/NavbarActions.tsx new file mode 100644 index 00000000..0b36c516 --- /dev/null +++ b/apps/app/components/Sidebar/NavbarActions.tsx @@ -0,0 +1,16 @@ +import { SidebarTrigger, useSidebarSafe } from "@/components/shadcn/sidebar" +import { WalletsHeader } from "@/components/Wallet/ConnectedWallets" + +export default function NavbarActions() { + const sidebar = useSidebarSafe() + + if (!sidebar) return null + if (sidebar.open) return null + + return ( +
+ + +
+ ) +} diff --git a/apps/app/components/Sidebar/SidebarMenuList.tsx b/apps/app/components/Sidebar/SidebarMenuList.tsx new file mode 100644 index 00000000..1d146cbb --- /dev/null +++ b/apps/app/components/Sidebar/SidebarMenuList.tsx @@ -0,0 +1,11 @@ +import { FC } from "react" +import MenuList from "@/components/TrainMenu/MenuList" +import { MenuStep } from "@/Models/Wizard" + +const SidebarMenuList: FC<{ goToStep: (step: MenuStep) => void; onViewSwap?: (hashlock: string) => void }> = ({ goToStep, onViewSwap }) => { + return
+ +
+} + +export default SidebarMenuList diff --git a/apps/app/components/TrainMenu/Menu.tsx b/apps/app/components/TrainMenu/Menu.tsx index 1353232d..14fbcc97 100644 --- a/apps/app/components/TrainMenu/Menu.tsx +++ b/apps/app/components/TrainMenu/Menu.tsx @@ -3,9 +3,8 @@ import LinkWrapper from "../LinkWraapper" import { ReactNode } from "react" import { motion } from "framer-motion"; import { useEffect, useRef, useState } from "react" - const Menu = ({ children }: { children: ReactNode }) => { - return
+ return
{children}
} diff --git a/apps/app/components/TrainMenu/MenuList.tsx b/apps/app/components/TrainMenu/MenuList.tsx index 1e98c7a9..c08376e3 100644 --- a/apps/app/components/TrainMenu/MenuList.tsx +++ b/apps/app/components/TrainMenu/MenuList.tsx @@ -9,8 +9,6 @@ import inIframe from "@/components/utils/inIframe"; import GitHubLogo from "@/components/Icons/GitHubLogo"; import TwitterLogo from "@/components/Icons/TwitterLogo"; import Link from "next/link"; -import VaulDrawer from "@/components/Modal/vaulModal"; -import SendFeedback from "@/components/sendFeedback"; import Menu from "./Menu"; import dynamic from "next/dynamic"; import { MenuStep } from "@/Models/Wizard"; @@ -25,7 +23,6 @@ const MenuList: FC<{ goToStep: (step: MenuStep, path?: string) => void }> = ({ g const router = useRouter(); const { boot, show, update } = useIntercom() const [embedded, setEmbedded] = useState() - const [openFeedbackModal, setOpenFeedbackModal] = useState(false); const { isMobile } = useWindowDimensions() const { autoRevealSecret, setAutoRevealSecret } = useSwapPreferencesStore() const { theme, setTheme } = useTheme() @@ -34,9 +31,6 @@ const MenuList: FC<{ goToStep: (step: MenuStep, path?: string) => void }> = ({ g setEmbedded(inIframe()) }, []) - const handleCloseFeedback = () => { - setOpenFeedbackModal(false) - } return
@@ -121,25 +115,12 @@ const MenuList: FC<{ goToStep: (step: MenuStep, path?: string) => void }> = ({ g - setOpenFeedbackModal(true)} target="_blank" icon={}> + goToStep(MenuStep.SuggestFeature)} icon={}> Suggest a Feature - - -
- -
-
-
- -
+ +

Media links & suggestions:

diff --git a/apps/app/components/TrainMenu/index.tsx b/apps/app/components/TrainMenu/index.tsx index 23a832e4..23ea6a5e 100644 --- a/apps/app/components/TrainMenu/index.tsx +++ b/apps/app/components/TrainMenu/index.tsx @@ -11,9 +11,10 @@ import { resolvePersistantQueryParams } from "@/helpers/querryHelper"; import { Modal, ModalContent } from "@/components/Modal/modalWithoutAnimation"; import RpcNetworkListView from "@/components/Settings/RpcNetworkListView"; import NetworkRpcEditView from "@/components/Settings/NetworkRpcEditView"; -import { Network } from "@/Models/Network"; import RecoverSwap from "@/components/Swap/Atomic/RecoverSwap"; import SwapHistory from "@/components/SwapHistory"; +import { useMenuNavigation } from "@/hooks/useMenuNavigation"; +import SendFeedback from "@/components/sendFeedback"; const Comp = () => { const router = useRouter(); @@ -22,16 +23,22 @@ const Comp = () => { const { goBack, currentStepName } = useFormWizardState() const { goToStep } = useFormWizardaUpdate() - const [selectedNetwork, setSelectedNetwork] = useState(null); + const { + selectedNetwork, + setSelectedNetwork, + goBackToRpcConfiguration, + handleNetworkSelect, + handleNetworkSave, + handleRecoverSwap, + } = useMenuNavigation({ onClose: () => setIsOpen(false) }) - const goBackToMenuStep = () => { goToStep(MenuStep.Menu, "back"); clearMenuPath(router) } - const goBackToRpcConfiguration = () => { goToStep(MenuStep.RPCConfiguration, "back") } - - const handleRecoverSwap = (hashlock: string) => { - setIsOpen(false) - router.push({ pathname: '/swap', query: { hashlock } }) + // Wrap to add URL history cleanup + const goBackToMenuStep = () => { + goToStep(MenuStep.Menu, "back") + clearMenuPath(router) } + // Wrap to add URL history push const handleGoToStep = (step: MenuStep, path?: string) => { goToStep(step) if (path) { @@ -39,16 +46,6 @@ const Comp = () => { } } - const handleNetworkSelect = (network: Network) => { - setSelectedNetwork(network) - goToStep(MenuStep.NetworkRPCEdit) - } - - const handleNetworkSave = () => { - setSelectedNetwork(null) - goToStep(MenuStep.RPCConfiguration, "back") - } - useEffect(() => { if (!isOpen) { goToStep(MenuStep.Menu) @@ -104,6 +101,9 @@ const Comp = () => { + + +
)} diff --git a/apps/app/components/Wallet/ConnectedWallets.tsx b/apps/app/components/Wallet/ConnectedWallets.tsx index 657c5fc0..4d08f83b 100644 --- a/apps/app/components/Wallet/ConnectedWallets.tsx +++ b/apps/app/components/Wallet/ConnectedWallets.tsx @@ -6,30 +6,57 @@ import { useState } from "react" import WalletsList from "./WalletsList" import { Wallet } from "../../Models/WalletProvider" import VaulDrawer from "../Modal/vaulModal" +import { ChevronDown } from "lucide-react" -export const WalletsHeader = () => { +type WalletsHeaderVariant = "mobile" | "navbar" + +const variantStyles: Record = { + mobile: "p-1.5 max-sm:p-2 text-secondary-text hover:bg-secondary-500 max-sm:bg-secondary-500 hover:text-primary-text focus:outline-hidden inline-flex rounded-lg items-center active:animate-press-down", + navbar: "p-1.5 text-secondary-text bg-secondary-500 hover:bg-secondary-400 hover:text-primary-text focus:outline-hidden inline-flex rounded-lg items-center active:animate-press-down", +} + +export const WalletsHeader = ({ variant = "mobile" }: { variant?: WalletsHeaderVariant }) => { const { wallets } = useWallet() if (wallets.length > 0) { return ( - + ) } return ( -
+
) } -const WalletsHeaderWalletsList = ({ wallets }: { wallets: Wallet[] }) => { +const WalletsHeaderWalletsList = ({ wallets, variant = "mobile" }: { wallets: Wallet[]; variant?: WalletsHeaderVariant }) => { const [openModal, setOpenModal] = useState(false) + const wallet = wallets[0] + return <> - { } return ( -
+

© {new Date().getFullYear()} Layerswap Labs, Inc. All rights reserved. diff --git a/apps/app/components/layout.tsx b/apps/app/components/layout.tsx index 07f1034c..7856547d 100644 --- a/apps/app/components/layout.tsx +++ b/apps/app/components/layout.tsx @@ -19,6 +19,8 @@ import { AtomicProvider } from "@/context/atomicContext"; import { SwapAccountsProvider } from "@/context/swapAccounts"; import { LoginModal } from "./SecretDerivation"; import { useLoginModalStore } from "@/stores/loginModalStore"; +import { SidebarProvider } from "./shadcn/sidebar"; + type Props = { children: JSX.Element | JSX.Element[]; hideFooter?: boolean; @@ -100,23 +102,25 @@ export default function Layout({ children, settings }: Props) { - + - - - - - {process.env.NEXT_PUBLIC_IN_MAINTANANCE === 'true' ? - - : children} - - - + + + + + + {process.env.NEXT_PUBLIC_IN_MAINTANANCE === 'true' ? + + : children} + + + + - + diff --git a/apps/app/components/navbar.tsx b/apps/app/components/navbar.tsx index 8e74d980..5f024abe 100644 --- a/apps/app/components/navbar.tsx +++ b/apps/app/components/navbar.tsx @@ -4,12 +4,7 @@ import Link from 'next/link'; import { JetBrains_Mono } from "next/font/google"; import clsx from 'clsx'; import { ArrowUpRight } from 'lucide-react'; -import dynamic from 'next/dynamic'; -import PendingSwap from './Swap/PendingSwap'; - -const UserStatusHeader = dynamic(() => import("./SecretDerivation/UserStatus").then((comp) => comp.UserStatusHeader), { - loading: () => <> -}) +import NavbarActions from './Sidebar/NavbarActions'; const jetBrainsMono = JetBrains_Mono({ variable: "--font-jb-mono", @@ -53,10 +48,7 @@ export default function Navbar() { }

-
- - -
+
) diff --git a/apps/app/components/sendFeedback.tsx b/apps/app/components/sendFeedback.tsx index fe3b2e13..067777fa 100644 --- a/apps/app/components/sendFeedback.tsx +++ b/apps/app/components/sendFeedback.tsx @@ -64,7 +64,7 @@ const SendFeedback: FC = ({ onSend }) => { onChange={e => { handleChange(e) }} - className="h-40 max-h-60 appearance-none block bg-secondary-500 text-primary-text border border-secondary-500 rounded-md py-3 px-4 mb-3 leading-tight focus:ring-0 focus:bg-secondary-500 focus:border-secondary-100 " + className="h-72 max-h-96 appearance-none block bg-secondary-500 text-primary-text border border-secondary-500 rounded-md py-3 px-4 mb-3 leading-tight focus:ring-0 focus:bg-secondary-500 focus:border-secondary-100 " /> + +
+ {children} +
+
+ + ) +} + +// --- Trigger --- + +type SidebarTriggerProps = { + className?: string + variant?: "mobile" | "navbar" +} + +export const SidebarTrigger: FC = ({ className, variant = "mobile" }) => { + const { toggleSidebar, isMobile } = useSidebar() + + if (isMobile) return null + + const baseClassName = variant === "navbar" + ? "active:animate-press-down text-secondary-text bg-secondary-500 hover:bg-secondary-400 hover:text-primary-text focus:outline-hidden rounded-lg items-center inline-flex py-1.5" + : "active:animate-press-down text-secondary-text hover:bg-secondary-500 hover:text-primary-text focus:outline-hidden rounded-lg items-center inline-flex py-1.5" + + return ( + + ) +} + +// --- Content (scrollable area) --- + +export const SidebarContent: FC<{ children: ReactNode; className?: string }> = ({ children, className }) => { + return ( +
+ {children} +
+ ) +} + +// --- Header --- + +export const SidebarHeader: FC<{ children: ReactNode; className?: string }> = ({ children, className }) => { + return ( +
+ {children} +
+ ) +} diff --git a/apps/app/components/themeWrapper.tsx b/apps/app/components/themeWrapper.tsx index 33d0eaab..431e36fc 100644 --- a/apps/app/components/themeWrapper.tsx +++ b/apps/app/components/themeWrapper.tsx @@ -2,6 +2,7 @@ import { X } from "lucide-react"; import toast, { ToastBar, Toaster } from "react-hot-toast" import Navbar from "./navbar" import GlobalFooter from "./globalFooter"; +import AppSidebar from "./Sidebar/AppSidebar"; type Props = { children: JSX.Element | JSX.Element[] @@ -9,47 +10,48 @@ type Props = { export default function ThemeWrapper({ children }: Props) { return
-
-
- - {(t) => ( - - {({ icon, message }) => ( - <> - {icon} - {message} - {t.type !== 'loading' && ( - - )} - - )} - - )} - - -
-
-
-
- {children} +
+
+ + {(t) => ( + + {({ icon, message }) => ( + <> + {icon} + {message} + {t.type !== 'loading' && ( + + )} + + )} + + )} + + +
+
+
+
+ {children} +
+
+
-
- -
+
} diff --git a/apps/app/hooks/useMenuNavigation.ts b/apps/app/hooks/useMenuNavigation.ts new file mode 100644 index 00000000..68b23992 --- /dev/null +++ b/apps/app/hooks/useMenuNavigation.ts @@ -0,0 +1,68 @@ +import { useState, useCallback } from "react" +import { useFormWizardaUpdate } from "@/context/formWizardProvider" +import { MenuStep } from "@/Models/Wizard" +import { Network } from "@/Models/Network" +import { useRouter } from "next/router" +import { useSwapStore } from "@/stores/swapStore" + +type UseMenuNavigationOptions = { + onClose: () => void +} + +export function useMenuNavigation({ onClose }: UseMenuNavigationOptions) { + const { goToStep } = useFormWizardaUpdate() + const router = useRouter() + const setActiveHashlock = useSwapStore(s => s.setActiveHashlock) + const setSwapModalOpen = useSwapStore(s => s.setSwapModalOpen) + + const [selectedNetwork, setSelectedNetwork] = useState(null) + + const goBackToMenuStep = useCallback(() => { + goToStep(MenuStep.Menu, "back") + }, [goToStep]) + + const goBackToRpcConfiguration = useCallback(() => { + goToStep(MenuStep.RPCConfiguration, "back") + }, [goToStep]) + + const handleGoToStep = useCallback((step: MenuStep) => { + goToStep(step) + }, [goToStep]) + + const handleNetworkSelect = useCallback((network: Network) => { + setSelectedNetwork(network) + goToStep(MenuStep.NetworkRPCEdit) + }, [goToStep]) + + const handleNetworkSave = useCallback(() => { + setSelectedNetwork(null) + goToStep(MenuStep.RPCConfiguration, "back") + }, [goToStep]) + + const handleRecoverSwap = useCallback((hashlock: string) => { + onClose() + router.push({ pathname: '/swap', query: { hashlock } }) + }, [onClose, router]) + + const handleViewSwap = useCallback((hashlock: string) => { + onClose() + if (router.pathname === '/') { + setActiveHashlock(hashlock) + setSwapModalOpen(true) + } else { + router.push({ pathname: '/swap', query: { hashlock } }) + } + }, [onClose, router, setActiveHashlock, setSwapModalOpen]) + + return { + selectedNetwork, + setSelectedNetwork, + goBackToMenuStep, + goBackToRpcConfiguration, + handleGoToStep, + handleNetworkSelect, + handleNetworkSave, + handleRecoverSwap, + handleViewSwap, + } +} From e08f1cce9e09975c98f93cc763f1184b21cd4b98 Mon Sep 17 00:00:00 2001 From: yasha-meursault Date: Mon, 30 Mar 2026 15:42:42 +0400 Subject: [PATCH 02/19] implement shadcn sidebar instead of custom one --- apps/app/components/Sidebar/AppSidebar.tsx | 2 +- apps/app/components/Sidebar/NavbarActions.tsx | 14 +- apps/app/components/layout.tsx | 4 +- apps/app/components/navbar.tsx | 2 +- apps/app/components/shadcn/button.tsx | 46 ++ apps/app/components/shadcn/input.tsx | 23 + apps/app/components/shadcn/separator.tsx | 32 + apps/app/components/shadcn/sheet.tsx | 127 +++ apps/app/components/shadcn/sidebar.tsx | 760 +++++++++++++++--- apps/app/components/shadcn/skeleton.tsx | 17 + apps/app/components/themeWrapper.tsx | 5 +- 11 files changed, 915 insertions(+), 117 deletions(-) create mode 100644 apps/app/components/shadcn/button.tsx create mode 100644 apps/app/components/shadcn/input.tsx create mode 100644 apps/app/components/shadcn/separator.tsx create mode 100644 apps/app/components/shadcn/sheet.tsx create mode 100644 apps/app/components/shadcn/skeleton.tsx diff --git a/apps/app/components/Sidebar/AppSidebar.tsx b/apps/app/components/Sidebar/AppSidebar.tsx index db4c9c84..6c6e1536 100644 --- a/apps/app/components/Sidebar/AppSidebar.tsx +++ b/apps/app/components/Sidebar/AppSidebar.tsx @@ -15,7 +15,7 @@ import SendFeedback from "@/components/sendFeedback" const AppSidebar: FC = () => { return ( - + diff --git a/apps/app/components/Sidebar/NavbarActions.tsx b/apps/app/components/Sidebar/NavbarActions.tsx index 0b36c516..da3dfe73 100644 --- a/apps/app/components/Sidebar/NavbarActions.tsx +++ b/apps/app/components/Sidebar/NavbarActions.tsx @@ -1,5 +1,6 @@ -import { SidebarTrigger, useSidebarSafe } from "@/components/shadcn/sidebar" +import { useSidebarSafe } from "@/components/shadcn/sidebar" import { WalletsHeader } from "@/components/Wallet/ConnectedWallets" +import { MenuIcon } from "lucide-react" export default function NavbarActions() { const sidebar = useSidebarSafe() @@ -10,7 +11,16 @@ export default function NavbarActions() { return (
- +
) } diff --git a/apps/app/components/layout.tsx b/apps/app/components/layout.tsx index 7856547d..34a55fe0 100644 --- a/apps/app/components/layout.tsx +++ b/apps/app/components/layout.tsx @@ -19,7 +19,7 @@ import { AtomicProvider } from "@/context/atomicContext"; import { SwapAccountsProvider } from "@/context/swapAccounts"; import { LoginModal } from "./SecretDerivation"; import { useLoginModalStore } from "@/stores/loginModalStore"; -import { SidebarProvider } from "./shadcn/sidebar"; + type Props = { children: JSX.Element | JSX.Element[]; @@ -102,7 +102,6 @@ export default function Layout({ children, settings }: Props) { - @@ -120,7 +119,6 @@ export default function Layout({ children, settings }: Props) { - diff --git a/apps/app/components/navbar.tsx b/apps/app/components/navbar.tsx index 5f024abe..9e214db1 100644 --- a/apps/app/components/navbar.tsx +++ b/apps/app/components/navbar.tsx @@ -21,7 +21,7 @@ export default function Navbar() { ] return ( -
+
diff --git a/apps/app/components/shadcn/button.tsx b/apps/app/components/shadcn/button.tsx new file mode 100644 index 00000000..1939debf --- /dev/null +++ b/apps/app/components/shadcn/button.tsx @@ -0,0 +1,46 @@ +"use client" + +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { classNames } from "../utils/classNames" + +const BUTTON_BASE = "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50" + +const BUTTON_VARIANTS: Record = { + default: "bg-primary-500 text-primary-buttonTextColor hover:bg-primary-600", + destructive: "bg-error-foreground text-primary-text hover:bg-error-foreground/90", + outline: "border border-secondary-400 bg-secondary-700 hover:bg-secondary-500 hover:text-primary-text", + secondary: "bg-secondary-500 text-primary-text hover:bg-secondary-400", + ghost: "hover:bg-secondary-500 hover:text-primary-text", + link: "text-primary-500 underline-offset-4 hover:underline", +} + +const BUTTON_SIZES: Record = { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + "icon-sm": "h-7 w-7", +} + +export interface ButtonProps extends React.ButtonHTMLAttributes { + asChild?: boolean + variant?: keyof typeof BUTTON_VARIANTS + size?: keyof typeof BUTTON_SIZES +} + +const Button = React.forwardRef( + ({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button } diff --git a/apps/app/components/shadcn/input.tsx b/apps/app/components/shadcn/input.tsx new file mode 100644 index 00000000..0d43a315 --- /dev/null +++ b/apps/app/components/shadcn/input.tsx @@ -0,0 +1,23 @@ +"use client" + +import * as React from "react" +import { classNames } from "../utils/classNames" + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/apps/app/components/shadcn/separator.tsx b/apps/app/components/shadcn/separator.tsx new file mode 100644 index 00000000..2a1a2c7f --- /dev/null +++ b/apps/app/components/shadcn/separator.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import { classNames } from "../utils/classNames" + +const Separator = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + orientation?: "horizontal" | "vertical" + decorative?: boolean + } +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( +
+ ) +) +Separator.displayName = "Separator" + +export { Separator } diff --git a/apps/app/components/shadcn/sheet.tsx b/apps/app/components/shadcn/sheet.tsx new file mode 100644 index 00000000..95dc9aca --- /dev/null +++ b/apps/app/components/shadcn/sheet.tsx @@ -0,0 +1,127 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" +import { classNames } from "../utils/classNames" + +const Sheet = DialogPrimitive.Root + +const SheetTrigger = DialogPrimitive.Trigger + +const SheetClose = DialogPrimitive.Close + +const SheetPortal = DialogPrimitive.Portal + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetOverlay.displayName = DialogPrimitive.Overlay.displayName + +const sheetVariants = { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + right: "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", +} + +interface SheetContentProps + extends React.ComponentPropsWithoutRef { + side?: keyof typeof sheetVariants +} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + {children} + + +)) +SheetContent.displayName = DialogPrimitive.Content.displayName + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetHeader.displayName = "SheetHeader" + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetFooter.displayName = "SheetFooter" + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetTitle.displayName = DialogPrimitive.Title.displayName + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetDescription.displayName = DialogPrimitive.Description.displayName + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/apps/app/components/shadcn/sidebar.tsx b/apps/app/components/shadcn/sidebar.tsx index 34cce30b..6a40face 100644 --- a/apps/app/components/shadcn/sidebar.tsx +++ b/apps/app/components/shadcn/sidebar.tsx @@ -1,150 +1,694 @@ -import { createContext, useContext, useCallback, useState, useMemo, useEffect, FC, ReactNode } from "react" -import { MenuIcon, ChevronsRight } from "lucide-react" +"use client" + +import * as React from "react" +import { ChevronsRight, PanelLeftIcon } from "lucide-react" +import { Slot } from "@radix-ui/react-slot" + import useWindowDimensions from "@/hooks/useWindowDimensions" import { classNames } from "@/components/utils/classNames" +import { Input } from "@/components/shadcn/input" +import { Separator } from "@/components/shadcn/separator" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/shadcn/sheet" +import { Skeleton } from "@/components/shadcn/skeleton" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/shadcn/tooltip" -const SIDEBAR_WIDTH = 400 - -// --- Context --- +const SIDEBAR_COOKIE_NAME = "sidebar_state" +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +const SIDEBAR_WIDTH = "400px" +const SIDEBAR_WIDTH_MOBILE = "18rem" +const SIDEBAR_WIDTH_ICON = "3rem" +const SIDEBAR_KEYBOARD_SHORTCUT = "b" -type SidebarContextType = { - open: boolean - setOpen: (open: boolean) => void - toggleSidebar: () => void - isMobile: boolean +type SidebarContextProps = { + state: "expanded" | "collapsed" + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void } -const SidebarContext = createContext(null) +const SidebarContext = React.createContext(null) -export function useSidebar() { - const ctx = useContext(SidebarContext) - if (!ctx) throw new Error("useSidebar must be used within SidebarProvider") - return ctx +function useSidebar() { + const context = React.useContext(SidebarContext) + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider.") + } + + return context } -export function useSidebarSafe() { - return useContext(SidebarContext) +function useSidebarSafe() { + return React.useContext(SidebarContext) } -// --- Provider --- +function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: React.ComponentProps<"div"> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void +}) { + const { isMobile } = useWindowDimensions() + const [openMobile, setOpenMobile] = React.useState(false) -type SidebarProviderProps = { - children: ReactNode - defaultOpen?: boolean -} + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen) + const open = openProp ?? _open + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } -export const SidebarProvider: FC = ({ children, defaultOpen = false }) => { - const { isMobile } = useWindowDimensions() - const [open, setOpen] = useState(defaultOpen) + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) - const toggleSidebar = useCallback(() => setOpen(prev => !prev), []) + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) - // Close sidebar when switching to mobile - useEffect(() => { - if (isMobile && open) setOpen(false) - }, [isMobile]) + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed" + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + +
+ {children} +
+
+ ) +} - const value = useMemo( - () => ({ open, setOpen, toggleSidebar, isMobile }), - [open, setOpen, toggleSidebar, isMobile] +function Sidebar({ + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + dir, + ...props +}: React.ComponentProps<"div"> & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" +}) { + const { isMobile, state, open, openMobile, setOpenMobile, toggleSidebar } = useSidebar() + + if (collapsible === "none") { + return ( +
+ {children} +
) + } + if (isMobile) { return ( - - {children} - + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
) + } + + return ( + + ) } -// --- Sidebar Panel --- +function SidebarTrigger({ + className, + onClick, + ...props +}: React.ComponentProps<"button">) { + const { toggleSidebar } = useSidebar() -type SidebarProps = { - children: ReactNode - className?: string + return ( + + ) } -export const Sidebar: FC = ({ children, className }) => { - const { open, isMobile, toggleSidebar } = useSidebar() +function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { + const { toggleSidebar } = useSidebar() - if (isMobile) return null + return ( + - -
- {children} -
-
- - ) +function SidebarInset({ className, ...props }: React.ComponentProps<"main">) { + return ( +
+ ) } -// --- Trigger --- +function SidebarInput({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} -type SidebarTriggerProps = { - className?: string - variant?: "mobile" | "navbar" +function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) } -export const SidebarTrigger: FC = ({ className, variant = "mobile" }) => { - const { toggleSidebar, isMobile } = useSidebar() +function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} - if (isMobile) return null +function SidebarSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} - const baseClassName = variant === "navbar" - ? "active:animate-press-down text-secondary-text bg-secondary-500 hover:bg-secondary-400 hover:text-primary-text focus:outline-hidden rounded-lg items-center inline-flex py-1.5" - : "active:animate-press-down text-secondary-text hover:bg-secondary-500 hover:text-primary-text focus:outline-hidden rounded-lg items-center inline-flex py-1.5" +function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} - return ( - - ) +function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) } -// --- Content (scrollable area) --- +function SidebarGroupLabel({ + className, + asChild = false, + ...props +}: React.ComponentProps<"div"> & { asChild?: boolean }) { + const Comp = asChild ? Slot : "div" -export const SidebarContent: FC<{ children: ReactNode; className?: string }> = ({ children, className }) => { - return ( -
- {children} -
- ) + return ( + svg]:size-4 [&>svg]:shrink-0", + className + )} + {...props} + /> + ) } -// --- Header --- +function SidebarGroupAction({ + className, + asChild = false, + ...props +}: React.ComponentProps<"button"> & { asChild?: boolean }) { + const Comp = asChild ? Slot : "button" -export const SidebarHeader: FC<{ children: ReactNode; className?: string }> = ({ children, className }) => { - return ( -
- {children} -
- ) + return ( + svg]:size-4 [&>svg]:shrink-0", + className + )} + {...props} + /> + ) +} + +function SidebarGroupContent({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) { + return ( +
    + ) +} + +function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { + return ( +
  • + ) +} + +const SIDEBAR_MENU_BUTTON_BASE = "peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-secondary-text outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-secondary-500 hover:text-primary-text focus-visible:ring-2 active:bg-secondary-500 active:text-primary-text disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-secondary-500 data-open:hover:text-primary-text data-active:bg-secondary-500 data-active:font-medium data-active:text-primary-text [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate" + +const SIDEBAR_MENU_BUTTON_VARIANTS: Record = { + default: "hover:bg-secondary-500 hover:text-primary-text", + outline: "bg-secondary-700 shadow-[0_0_0_1px_rgb(var(--ls-colors-secondary-400))] hover:bg-secondary-500 hover:text-primary-text hover:shadow-[0_0_0_1px_rgb(var(--ls-colors-secondary-400))]", +} + +const SIDEBAR_MENU_BUTTON_SIZES: Record = { + default: "h-8 text-sm", + sm: "h-7 text-xs", + lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", +} + +function sidebarMenuButtonVariants({ variant = "default", size = "default" }: { variant?: string; size?: string }) { + return classNames(SIDEBAR_MENU_BUTTON_BASE, SIDEBAR_MENU_BUTTON_VARIANTS[variant], SIDEBAR_MENU_BUTTON_SIZES[size]) +} + +function SidebarMenuButton({ + asChild = false, + isActive = false, + variant = "default", + size = "default", + tooltip, + className, + ...props +}: React.ComponentProps<"button"> & { + asChild?: boolean + isActive?: boolean + tooltip?: string | React.ComponentProps + variant?: "default" | "outline" + size?: "default" | "sm" | "lg" +}) { + const Comp = asChild ? Slot : "button" + const { isMobile, state } = useSidebar() + + const button = ( + + ) + + if (!tooltip) { + return button + } + + if (typeof tooltip === "string") { + tooltip = { + children: tooltip, + } + } + + return ( + + {button} + + ) +} + +function SidebarMenuAction({ + className, + asChild = false, + showOnHover = false, + ...props +}: React.ComponentProps<"button"> & { + asChild?: boolean + showOnHover?: boolean +}) { + const Comp = asChild ? Slot : "button" + + return ( + svg]:size-4 [&>svg]:shrink-0", + showOnHover && + "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-primary-text aria-expanded:opacity-100 md:opacity-0", + className + )} + {...props} + /> + ) +} + +function SidebarMenuBadge({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function SidebarMenuSkeleton({ + className, + showIcon = false, + ...props +}: React.ComponentProps<"div"> & { + showIcon?: boolean +}) { + // Random width between 50 to 90%. + const [width] = React.useState(() => { + return `${Math.floor(Math.random() * 40) + 50}%` + }) + + return ( +
    + {showIcon && ( + + )} + +
    + ) +} + +function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { + return ( +
      + ) +} + +function SidebarMenuSubItem({ + className, + ...props +}: React.ComponentProps<"li">) { + return ( +
    • + ) +} + +function SidebarMenuSubButton({ + asChild = false, + size = "md", + isActive = false, + className, + ...props +}: React.ComponentProps<"a"> & { + asChild?: boolean + size?: "sm" | "md" + isActive?: boolean +}) { + const Comp = asChild ? Slot : "a" + + return ( + span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-primary-text", + className + )} + {...props} + /> + ) +} + +export { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupAction, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarInput, + SidebarInset, + SidebarMenu, + SidebarMenuAction, + SidebarMenuBadge, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSkeleton, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarProvider, + SidebarRail, + SidebarSeparator, + SidebarTrigger, + useSidebar, + useSidebarSafe, } diff --git a/apps/app/components/shadcn/skeleton.tsx b/apps/app/components/shadcn/skeleton.tsx new file mode 100644 index 00000000..2c04084b --- /dev/null +++ b/apps/app/components/shadcn/skeleton.tsx @@ -0,0 +1,17 @@ +"use client" + +import { classNames } from "../utils/classNames" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
      + ) +} + +export { Skeleton } diff --git a/apps/app/components/themeWrapper.tsx b/apps/app/components/themeWrapper.tsx index 431e36fc..9dfec7b8 100644 --- a/apps/app/components/themeWrapper.tsx +++ b/apps/app/components/themeWrapper.tsx @@ -3,6 +3,7 @@ import toast, { ToastBar, Toaster } from "react-hot-toast" import Navbar from "./navbar" import GlobalFooter from "./globalFooter"; import AppSidebar from "./Sidebar/AppSidebar"; +import { SidebarProvider } from "./shadcn/sidebar"; type Props = { children: JSX.Element | JSX.Element[] @@ -10,7 +11,7 @@ type Props = { export default function ThemeWrapper({ children }: Props) { return
      -
      +
      -
      +
      } From 289e714786edb5c18182b6db970cb9e819663913 Mon Sep 17 00:00:00 2001 From: yasha-meursault Date: Mon, 30 Mar 2026 15:44:41 +0400 Subject: [PATCH 03/19] bring wallets button back to the picker --- apps/app/components/Input/RoutePicker/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/app/components/Input/RoutePicker/index.tsx b/apps/app/components/Input/RoutePicker/index.tsx index 3ecf5ea6..7d7db1af 100644 --- a/apps/app/components/Input/RoutePicker/index.tsx +++ b/apps/app/components/Input/RoutePicker/index.tsx @@ -11,7 +11,6 @@ import useWallet from "@/hooks/useWallet"; import useSuggestionsLimit from "@/hooks/useSuggestionsLimit"; import Balance from "@/components/Input/Amount/Balance"; import PickerWalletConnect from "./PickerWalletConnect"; -import useWindowDimensions from "@/hooks/useWindowDimensions"; const RoutePicker: FC<{ direction: SwapDirection, className?: string }> = ({ direction, className }) => { const { @@ -20,7 +19,6 @@ const RoutePicker: FC<{ direction: SwapDirection, className?: string }> = ({ dir } = useFormikContext(); const [searchQuery, setSearchQuery] = useState("") const { wallets } = useWallet() - const { isMobile } = useWindowDimensions() const { suggestionsLimit } = useSuggestionsLimit({ hasWallet: wallets.length > 0 }); @@ -46,7 +44,7 @@ const RoutePicker: FC<{ direction: SwapDirection, className?: string }> = ({ dir : undefined} + header={} > {({ closeModal }) => ( Date: Mon, 30 Mar 2026 15:55:59 +0400 Subject: [PATCH 04/19] small fixes --- .../SecretDerivation/LoginModal/index.tsx | 18 ++++++++---------- apps/app/components/Sidebar/AppSidebar.tsx | 5 ++--- apps/app/components/TrainMenu/index.tsx | 2 +- apps/app/hooks/useMenuNavigation.ts | 12 +++--------- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/apps/app/components/SecretDerivation/LoginModal/index.tsx b/apps/app/components/SecretDerivation/LoginModal/index.tsx index 3a44fcaa..1f500605 100644 --- a/apps/app/components/SecretDerivation/LoginModal/index.tsx +++ b/apps/app/components/SecretDerivation/LoginModal/index.tsx @@ -218,17 +218,15 @@ const Signing = ({ : (platformHint || 'Complete the action in your passkey or wallet.'); return ( -
      -
      -
      - {icon} -
      -
      -

      {title}

      -

      {subtitle}

      -
      +
      +
      + {icon} +
      +
      +

      {title}

      +

      {subtitle}

      -
      +
      {error && onRetry && (
      - {isMobile && ( + {isMobile ? (
      + ) : ( +
      + setSettingsOpen(true)} + aria-label="Settings" + className="inline-flex active:animate-press-down" + icon={} + /> +
      )} +
      ) } diff --git a/apps/app/components/SecretDerivation/LoginModal/PasskeyChoice.tsx b/apps/app/components/SecretDerivation/LoginModal/PasskeyChoice.tsx index bdbdd936..bbdb4d8c 100644 --- a/apps/app/components/SecretDerivation/LoginModal/PasskeyChoice.tsx +++ b/apps/app/components/SecretDerivation/LoginModal/PasskeyChoice.tsx @@ -32,9 +32,9 @@ export function PasskeyChoice({ error, onTryAgain, onCreateNew, onCrossDeviceLog
      {error && ( -
      - -

      +

      + +

      {error}

      diff --git a/apps/app/components/SecretDerivation/LoginModal/index.tsx b/apps/app/components/SecretDerivation/LoginModal/index.tsx index 1f500605..bdfbbbeb 100644 --- a/apps/app/components/SecretDerivation/LoginModal/index.tsx +++ b/apps/app/components/SecretDerivation/LoginModal/index.tsx @@ -1,25 +1,8 @@ -import { useEffect, useRef, useState } from 'react'; import { Loader2, ChevronLeft, CircleX, AlertTriangle } from 'lucide-react'; import VaulModal from '@/components/Modal/vaulModal'; -import { useSecretDerivation } from '@/context/secretDerivationContext'; -import { mapPasskeyError } from '@/lib/htlc/secretDerivation/passkeyService'; -import { PasskeyChoice } from './PasskeyChoice'; -// import { Wallet } from '@/Models/WalletProvider'; -import { useSteps } from '@/hooks/useSteps'; -import { Steps, Step } from '@/components/Step'; -// import OptionSelect from './OptionSelect'; import IconButton from '@/components/buttons/iconButton'; -// import WalletSelect from './SelectWallet'; -import { usePasskeyCredentialIds } from '@/stores/secretDerivationStore'; - -//type LoginStep = 'pick' | 'passkey_recovery' | 'wallet_select' | 'signing'; -type LoginStep = 'unsupported' | 'passkey_recovery' | 'signing'; - -// const getErrorMessage = (error: unknown, fallback: string): string => { -// if (error instanceof Error) return error.message; -// if (typeof error === 'string') return error; -// return fallback; -// }; +import { LoginSteps } from '../LoginSteps'; +import { usePasskeyLoginFlow } from '@/hooks/usePasskeyLoginFlow'; interface LoginModalProps { isOpen: boolean; @@ -27,133 +10,43 @@ interface LoginModalProps { } export function LoginModal({ isOpen, onClose }: LoginModalProps) { - const { loginWithPasskey, derivationMessage, prfSupportDetails, isReady } = useSecretDerivation(); - const storedPasskeyIds = usePasskeyCredentialIds(); - const hasStoredPasskeys = storedPasskeyIds.length > 0; - const { currentStep, goToStep, goBack, canGoBack, reset, isStep } = useSteps({ initial: 'signing' }); - const [passkeyError, setPasskeyError] = useState(null); - const [signingError, setSigningError] = useState(null); - const loginTriggered = useRef(false); - - const passkeyUnsupported = isReady && prfSupportDetails && !prfSupportDetails.supported; - - useEffect(() => { - if (isOpen) { - reset(); - setPasskeyError(null); - setSigningError(null); - loginTriggered.current = false; - } - }, [isOpen, reset]); - - const closeAndReset = () => { - onClose(); - }; - - const startPasskeyLogin = async (options?: { forceCreate?: boolean; crossDevice?: boolean }) => { - goToStep('signing'); - setPasskeyError(null); - try { - if (options?.forceCreate) { - await loginWithPasskey({ forceCreate: true, label: 'Train' }); - } else if (options?.crossDevice) { - await loginWithPasskey({ crossDevice: true }); - } else { - await loginWithPasskey(); - } - closeAndReset(); - } catch (e) { - const message = mapPasskeyError(e); - setPasskeyError(message); - goToStep('passkey_recovery', 'back'); - } - }; - - // Auto-trigger passkey login when modal opens, or show unsupported screen - useEffect(() => { - if (isOpen && isReady && !loginTriggered.current) { - loginTriggered.current = true; - if (passkeyUnsupported) { - goToStep('unsupported'); - } else { - startPasskeyLogin(hasStoredPasskeys ? {} : { forceCreate: true }); - } - } - }, [isOpen, isReady]); - - const handleBack = () => { - if (isStep('passkey_recovery')) { - setPasskeyError(null); - startPasskeyLogin(hasStoredPasskeys ? {} : { forceCreate: true }); - return; - } - goBack(); - }; + const loginFlow = usePasskeyLoginFlow({ + isActive: isOpen, + onSuccess: onClose, + onDismiss: onClose, + }); return ( { - if (!show) closeAndReset(); + if (!show) onClose(); }} header={
      { - canGoBack && + loginFlow.canGoBack &&
      - }>
      } -

      {currentStep === 'signing' ? 'Signing' : currentStep === 'unsupported' ? 'Browser not supported' : 'Login to continue'}

      +

      {loginFlow.currentStep === 'signing' ? 'Signing' : loginFlow.currentStep === 'unsupported' ? 'Browser not supported' : 'Login to continue'}

      } modalId="secret-derivation-login-modal" > - - - {/* Wallet login temporarily disabled — passkey is the default */} - {/* - startPasskeyLogin(hasStoredPasskeys ? {} : { forceCreate: true })} goToStep={goToStep} onConnectFinish={onConnectFinish} /> - */} - - - - - - - startPasskeyLogin(hasStoredPasskeys ? {} : { forceCreate: true })} - onCreateNew={() => startPasskeyLogin({ forceCreate: true })} - onCrossDeviceLogin={() => startPasskeyLogin({ crossDevice: true })} - /> - - - {/* - - */} - - - - - - +
      ); } -const UnsupportedBrowser = ({ onClose }: { onClose: () => void }) => { +export const UnsupportedBrowser = ({ onClose }: { onClose: () => void }) => { return (
      @@ -179,7 +72,7 @@ const UnsupportedBrowser = ({ onClose }: { onClose: () => void }) => { ); }; -const Signing = ({ +export const Signing = ({ derivationMessage, onRetry, onCancel, @@ -211,7 +104,7 @@ const Signing = ({ const title = error ? 'Failed' - : (derivationMessage || 'Please sign…'); + : (derivationMessage || 'Please sign\u2026'); const subtitle = error ? error diff --git a/apps/app/components/SecretDerivation/LoginSteps.tsx b/apps/app/components/SecretDerivation/LoginSteps.tsx new file mode 100644 index 00000000..45195198 --- /dev/null +++ b/apps/app/components/SecretDerivation/LoginSteps.tsx @@ -0,0 +1,54 @@ +import { FC } from 'react' +import { Steps, Step } from '@/components/Step' +import { PasskeyChoice } from './LoginModal/PasskeyChoice' +import { Signing, UnsupportedBrowser } from './LoginModal' +import type { LoginStep } from '@/hooks/usePasskeyLoginFlow' + +interface LoginStepsProps { + currentStep: LoginStep + passkeyError: string | null + signingError: string | null + derivationMessage: string + hasStoredPasskeys: boolean + startPasskeyLogin: (options?: { forceCreate?: boolean; crossDevice?: boolean }) => void + onDismiss: () => void +} + +export const LoginSteps: FC = ({ + currentStep, + passkeyError, + signingError, + derivationMessage, + hasStoredPasskeys, + startPasskeyLogin, + onDismiss, +}) => ( + + {/* Wallet login temporarily disabled — passkey is the default */} + {/* + startPasskeyLogin(hasStoredPasskeys ? {} : { forceCreate: true })} goToStep={goToStep} onConnectFinish={onConnectFinish} /> + */} + + + + + startPasskeyLogin(hasStoredPasskeys ? {} : { forceCreate: true })} + onCreateNew={() => startPasskeyLogin({ forceCreate: true })} + onCrossDeviceLogin={() => startPasskeyLogin({ crossDevice: true })} + /> + + {/* + + */} + + + + +) diff --git a/apps/app/components/SecretDerivation/UserStatus.tsx b/apps/app/components/SecretDerivation/UserStatus.tsx index 383f5d48..4142ec73 100644 --- a/apps/app/components/SecretDerivation/UserStatus.tsx +++ b/apps/app/components/SecretDerivation/UserStatus.tsx @@ -24,7 +24,7 @@ interface LoginDataCardProps { className?: string } -const LoginDataCard = ({ +export const LoginDataCard = ({ method, loginWallet, passkeyCredentialId, @@ -37,7 +37,7 @@ const LoginDataCard = ({
      -
      +
      Passkey {passkeyCredentialId && ( @@ -58,7 +58,7 @@ const LoginDataCard = ({
      -
      +
      {loginWallet?.displayName || 'EVM Wallet'} @@ -86,7 +86,7 @@ interface UserStatusContentProps { showPasskeyWarning?: boolean } -const UserStatusContent = ({ +export const UserStatusContent = ({ method, loginWallet, logout, @@ -171,15 +171,6 @@ const UserStatusContent = ({ ))}
      )} - - {showPasskeyWarning && method === 'passkey' && ( -
      -

      - Store your passkeys securely. Losing your passkey means losing access to your account and any associated funds permanently. -

      -
      - )} - ) } @@ -312,24 +305,24 @@ export const UserStatusMenu = () => { return ( <> void }> = ({ show, setShow }) => { + return ( + + + + + + + + ) +} + +const SettingsModalInner: FC = () => { + const { goBack, currentStepName, moving, wrapperWidth } = useFormWizardState() + const { setWrapperWidth, goToStep } = useFormWizardaUpdate() + const wrapperRef = useRef(null) + + const { + selectedNetwork, + handleNetworkSelect, + handleNetworkSave, + } = useMenuNavigation() + + const { autoRevealSecret, setAutoRevealSecret } = useSwapPreferencesStore() + const { theme, setTheme } = useTheme() + + useEffect(() => { + function handleResize() { + if (wrapperRef.current) { + setWrapperWidth(wrapperRef.current.offsetWidth) + } + } + window.addEventListener("resize", handleResize) + handleResize() + return () => window.removeEventListener("resize", handleResize) + }, []) + + useEffect(() => { + return () => { + goToStep(MenuStep.Menu) + } + }, []) + + return ( +
      + {currentStepName !== MenuStep.Menu && ( +
      + +

      {currentStepName as string}

      +
      + )} + +
      + + + + + } + checked={autoRevealSecret} + onChange={setAutoRevealSecret} + > + Auto Reveal Secret + + } + checked={theme === "light"} + onChange={(checked) => setTheme(checked ? "light" : "default")} + > + Light Mode + + goToStep(MenuStep.RPCConfiguration)} icon={}> + RPC Configuration + + + + + goToStep(MenuStep.Menu, "back")} inModal> + + + goToStep(MenuStep.RPCConfiguration, "back")} inModal> + {selectedNetwork ? ( + + ) : ( +
      Loading...
      + )} +
      +
      +
      +
      + ) +} + +export default SettingsModal diff --git a/apps/app/components/Sidebar/AppSidebar.tsx b/apps/app/components/Sidebar/AppSidebar.tsx index 01fe703d..b57434de 100644 --- a/apps/app/components/Sidebar/AppSidebar.tsx +++ b/apps/app/components/Sidebar/AppSidebar.tsx @@ -1,104 +1,98 @@ -import { FC, useEffect, useRef } from "react" -import { Sidebar, SidebarContent } from "@/components/shadcn/sidebar" -import { FormWizardProvider, useFormWizardaUpdate, useFormWizardState } from "@/context/formWizardProvider" -import { MenuStep } from "@/Models/Wizard" -import WizardItem from "@/components/Wizard/WizardItem" -import SidebarMenuList from "./SidebarMenuList" -import RpcNetworkListView from "@/components/Settings/RpcNetworkListView" -import NetworkRpcEditView from "@/components/Settings/NetworkRpcEditView" -import RecoverSwap from "@/components/Swap/Atomic/RecoverSwap" +import { FC, useCallback, useState } from "react" +import { Sidebar, SidebarContent, SidebarHeader } from "@/components/shadcn/sidebar" +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/shadcn/tabs" import SwapHistory from "@/components/SwapHistory" +import RecoverSwap from "@/components/Swap/Atomic/RecoverSwap" +import AuthBlock from "@/components/AuthBlock" +import { UserStatusContent } from "@/components/SecretDerivation/UserStatus" +import { LoginSteps } from "@/components/SecretDerivation/LoginSteps" +import { usePasskeyLoginFlow } from "@/hooks/usePasskeyLoginFlow" +import { useSecretDerivationStore } from "@/stores/secretDerivationStore" +import { useLoginModalStore } from "@/stores/loginModalStore" import { ChevronLeft } from "lucide-react" -import { AnimatePresence } from "framer-motion" -import { useMenuNavigation } from "@/hooks/useMenuNavigation" -import SendFeedback from "@/components/sendFeedback" +import { useRouter } from "next/router" + +type SidebarView = "tabs" | "loginStatus" const AppSidebar: FC = () => { - return ( - - - - - - ) -} + const [view, setView] = useState("tabs") + const router = useRouter() + const { method, loginWallet } = useSecretDerivationStore() + const loginActive = useLoginModalStore((s) => s.isOpen && s.target === 'sidebar') + const { open: openLogin, close: closeLogin } = useLoginModalStore() -const SidebarInner: FC = () => { - const { goBack, currentStepName, moving, wrapperWidth } = useFormWizardState() - const { setWrapperWidth } = useFormWizardaUpdate() - const wrapperRef = useRef(null) + const handleRecoverSwap = useCallback((hashlock: string) => { + router.push({ pathname: '/swap', query: { hashlock } }) + }, [router]) - const { - selectedNetwork, - goBackToMenuStep, - goBackToRpcConfiguration, - handleGoToStep, - handleNetworkSelect, - handleNetworkSave, - handleRecoverSwap, - handleViewSwap, - } = useMenuNavigation() + const dismissLogin = useCallback(() => { + closeLogin() + setView("tabs") + }, [closeLogin]) - // Measure width for WizardItem animations - useEffect(() => { - function handleResize() { - if (wrapperRef.current) { - setWrapperWidth(wrapperRef.current.offsetWidth) - } - } - window.addEventListener("resize", handleResize) - handleResize() - return () => window.removeEventListener("resize", handleResize) - }, []) + const loginFlow = usePasskeyLoginFlow({ + isActive: loginActive, + onSuccess: dismissLogin, + onDismiss: dismissLogin, + }) return ( - <> - {currentStepName !== MenuStep.Menu && ( -
      + + {!loginActive && view === "tabs" && ( + + { setView("tabs"); openLogin('sidebar') }} + onViewLoginStatus={() => setView("loginStatus")} + /> + + )} + + {(view === "loginStatus" || loginActive) && ( +
      -

      {currentStepName as string}

      +

      + {loginActive + ? (loginFlow.currentStep === 'signing' ? 'Signing' : loginFlow.currentStep === 'unsupported' ? 'Browser not supported' : 'Login') + : 'Login Status'} +

      )} - -
      - - - - - - - - - {selectedNetwork ? ( - - ) : ( -
      Loading...
      - )} -
      - - - - + + {!loginActive && view === "tabs" && ( + + + Transactions + Recover Swap + + - - - - -
      -
      + + + + + + )} + {!loginActive && view === "loginStatus" && ( + useSecretDerivationStore.getState().logout()} + onClose={() => setView("tabs")} + /> + )} + {loginActive && ( + + )}
      - +
      ) } diff --git a/apps/app/components/Sidebar/NavbarActions.tsx b/apps/app/components/Sidebar/NavbarActions.tsx index da3dfe73..e0c8698e 100644 --- a/apps/app/components/Sidebar/NavbarActions.tsx +++ b/apps/app/components/Sidebar/NavbarActions.tsx @@ -1,25 +1,30 @@ import { useSidebarSafe } from "@/components/shadcn/sidebar" -import { WalletsHeader } from "@/components/Wallet/ConnectedWallets" +import PendingSwap from "@/components/Swap/PendingSwap" +import { useSwapStore } from "@/stores/swapStore" import { MenuIcon } from "lucide-react" export default function NavbarActions() { const sidebar = useSidebarSafe() + const hasActiveSwap = useSwapStore(s => !!s.activeHashlock && !!s.swaps[s.activeHashlock!]) if (!sidebar) return null if (sidebar.open) return null return (
      - + {sidebar.isMobile && }
      ) diff --git a/apps/app/components/Sidebar/SidebarMenuList.tsx b/apps/app/components/Sidebar/SidebarMenuList.tsx deleted file mode 100644 index 1d146cbb..00000000 --- a/apps/app/components/Sidebar/SidebarMenuList.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { FC } from "react" -import MenuList from "@/components/TrainMenu/MenuList" -import { MenuStep } from "@/Models/Wizard" - -const SidebarMenuList: FC<{ goToStep: (step: MenuStep) => void; onViewSwap?: (hashlock: string) => void }> = ({ goToStep, onViewSwap }) => { - return
      - -
      -} - -export default SidebarMenuList diff --git a/apps/app/components/Swap/FormButton.tsx b/apps/app/components/Swap/FormButton.tsx index 6d448190..e35f3c36 100644 --- a/apps/app/components/Swap/FormButton.tsx +++ b/apps/app/components/Swap/FormButton.tsx @@ -35,14 +35,9 @@ const FormButton = ({ // Check derivation method first (before any other checks) if (!isLoggedIn) { return ( - <> - - Login to continue - - + + Login to continue + ); } diff --git a/apps/app/components/SwapHistory/HistorySummaryCard.tsx b/apps/app/components/SwapHistory/HistorySummaryCard.tsx index d4f49380..ebc61978 100644 --- a/apps/app/components/SwapHistory/HistorySummaryCard.tsx +++ b/apps/app/components/SwapHistory/HistorySummaryCard.tsx @@ -3,6 +3,8 @@ import { FC } from 'react' import { ImageWithFallback } from '@/components/Common/ImageWithFallback' import { SwapData } from '@/stores/swapStore' import { Network } from '@/Models/Network' +import { isTerminalStatus } from '@/Models/HTLCStatus' +import StatusIcons from './StatusIcons' type Props = { swap: SwapData @@ -15,92 +17,101 @@ const HistorySummaryCard: FC = ({ swap, sourceNetwork, destNetwork }) => const destToken = destNetwork?.tokens.find(t => t.symbol === swap.destination_asset) return ( -
      -
      - {/* Source */} -
      -
      -
      - {sourceToken?.logo ? ( - - ) : ( -
      + <> +
      +
      + {/* Source */} +
      +
      +
      + {sourceToken?.logo ? ( + + ) : ( +
      + )} +
      + {sourceNetwork?.logoUrl && ( +
      + +
      )}
      - {sourceNetwork?.logoUrl && ( -
      - +
      +
      + {swap.requestedAmount} + {swap.source_asset}
      - )} -
      -
      -
      - {swap.requestedAmount} - {swap.source_asset} + + {sourceNetwork?.displayName ?? swap.source} +
      - - {sourceNetwork?.displayName ?? swap.source} -
      -
      - {/* Center arrow */} -
      -
      - + {/* Center arrow */} +
      +
      + +
      -
      - {/* Destination */} -
      -
      -
      - {swap.receiveAmount ?? '—'} - {swap.destination_asset} + {/* Destination */} +
      +
      +
      + {swap.receiveAmount ?? '—'} + {swap.destination_asset} +
      + + {destNetwork?.displayName ?? swap.destination} +
      - - {destNetwork?.displayName ?? swap.destination} - -
      -
      -
      - {destToken?.logo ? ( - - ) : ( -
      +
      +
      + {destToken?.logo ? ( + + ) : ( +
      + )} +
      + {destNetwork?.logoUrl && ( +
      + +
      )}
      - {destNetwork?.logoUrl && ( -
      - -
      - )}
      -
      + {swap.status && !isTerminalStatus(swap.status) && ( +
      +
      + +
      +
      + )} + ) } diff --git a/apps/app/components/SwapHistory/StatusIcons.tsx b/apps/app/components/SwapHistory/StatusIcons.tsx index 9b79ab0e..a4551a7c 100644 --- a/apps/app/components/SwapHistory/StatusIcons.tsx +++ b/apps/app/components/SwapHistory/StatusIcons.tsx @@ -21,23 +21,23 @@ export default function StatusIcons({ status }: { status: HTLCStatus | undefined case HTLCStatus.SolverLockDetected: case HTLCStatus.SecretRevealed: return ( - + - - + + In Progress ) case HTLCStatus.ManualClaimRequired: return ( - + Action Required ) case HTLCStatus.TimelockExpired: return ( - + Expired ) diff --git a/apps/app/components/TrainMenu/Menu.tsx b/apps/app/components/TrainMenu/Menu.tsx index 14fbcc97..6df18502 100644 --- a/apps/app/components/TrainMenu/Menu.tsx +++ b/apps/app/components/TrainMenu/Menu.tsx @@ -4,7 +4,7 @@ import { ReactNode } from "react" import { motion } from "framer-motion"; import { useEffect, useRef, useState } from "react" const Menu = ({ children }: { children: ReactNode }) => { - return
      + return
      {children}
      } diff --git a/apps/app/components/TrainMenu/MenuList.tsx b/apps/app/components/TrainMenu/MenuList.tsx index c08376e3..1b6820d4 100644 --- a/apps/app/components/TrainMenu/MenuList.tsx +++ b/apps/app/components/TrainMenu/MenuList.tsx @@ -13,9 +13,8 @@ import Menu from "./Menu"; import dynamic from "next/dynamic"; import { MenuStep } from "@/Models/Wizard"; import useWindowDimensions from "@/hooks/useWindowDimensions"; -import { UserStatusMenu } from "../SecretDerivation"; -const WalletsMenu = dynamic(() => import("../Wallet/ConnectedWallets").then((comp) => comp.WalletsMenu), { +const AuthBlock = dynamic(() => import("@/components/AuthBlock"), { loading: () => <> }) @@ -34,8 +33,7 @@ const MenuList: FC<{ goToStep: (step: MenuStep, path?: string) => void }> = ({ g return
      - - + @@ -120,7 +118,7 @@ const MenuList: FC<{ goToStep: (step: MenuStep, path?: string) => void }> = ({ g -
      +

      Media links & suggestions:

      diff --git a/apps/app/components/Wallet/ConnectedWallets.tsx b/apps/app/components/Wallet/ConnectedWallets.tsx index 4d08f83b..e30fa6c8 100644 --- a/apps/app/components/Wallet/ConnectedWallets.tsx +++ b/apps/app/components/Wallet/ConnectedWallets.tsx @@ -6,57 +6,30 @@ import { useState } from "react" import WalletsList from "./WalletsList" import { Wallet } from "../../Models/WalletProvider" import VaulDrawer from "../Modal/vaulModal" -import { ChevronDown } from "lucide-react" -type WalletsHeaderVariant = "mobile" | "navbar" - -const variantStyles: Record = { - mobile: "p-1.5 max-sm:p-2 text-secondary-text hover:bg-secondary-500 max-sm:bg-secondary-500 hover:text-primary-text focus:outline-hidden inline-flex rounded-lg items-center active:animate-press-down", - navbar: "p-1.5 text-secondary-text bg-secondary-500 hover:bg-secondary-400 hover:text-primary-text focus:outline-hidden inline-flex rounded-lg items-center active:animate-press-down", -} - -export const WalletsHeader = ({ variant = "mobile" }: { variant?: WalletsHeaderVariant }) => { +export const WalletsHeader = () => { const { wallets } = useWallet() if (wallets.length > 0) { return ( - + ) } return ( -
      +
      ) } -const WalletsHeaderWalletsList = ({ wallets, variant = "mobile" }: { wallets: Wallet[]; variant?: WalletsHeaderVariant }) => { +const WalletsHeaderWalletsList = ({ wallets }: { wallets: Wallet[] }) => { const [openModal, setOpenModal] = useState(false) - const wallet = wallets[0] - return <> - { return ( -
      +
      @@ -133,18 +106,18 @@ const WalletsMenuWalletsList = ({ wallets }: { wallets: Wallet[] }) => { diff --git a/apps/app/components/layout.tsx b/apps/app/components/layout.tsx index 34a55fe0..0d209916 100644 --- a/apps/app/components/layout.tsx +++ b/apps/app/components/layout.tsx @@ -29,7 +29,8 @@ type Props = { export default function Layout({ children, settings }: Props) { const router = useRouter(); - const { isOpen: loginOpen, close: closeLogin } = useLoginModalStore(); + const loginOpen = useLoginModalStore((s) => s.isOpen && s.target === 'modal'); + const closeLogin = useLoginModalStore((s) => s.close); if (!settings) return diff --git a/apps/app/components/shadcn/sheet.tsx b/apps/app/components/shadcn/sheet.tsx index 95dc9aca..41bbb27c 100644 --- a/apps/app/components/shadcn/sheet.tsx +++ b/apps/app/components/shadcn/sheet.tsx @@ -2,7 +2,6 @@ import * as React from "react" import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" import { classNames } from "../utils/classNames" const Sheet = DialogPrimitive.Root diff --git a/apps/app/components/shadcn/sidebar.tsx b/apps/app/components/shadcn/sidebar.tsx index 6a40face..ef3f5b8a 100644 --- a/apps/app/components/shadcn/sidebar.tsx +++ b/apps/app/components/shadcn/sidebar.tsx @@ -230,7 +230,7 @@ function Sidebar({ type="button" onClick={toggleSidebar} aria-label="Close sidebar" - className="absolute top-6 -left-8 -z-10 flex items-center justify-center w-10 h-10 rounded-l-xl bg-secondary-500 hover:bg-secondary-400 text-secondary-text hover:text-primary-text transition-colors cursor-pointer" + className="absolute top-4 -left-8 -z-10 flex items-center justify-center w-10 h-10 rounded-l-xl bg-secondary-500 hover:bg-secondary-400 text-secondary-text hover:text-primary-text transition-colors cursor-pointer" > diff --git a/apps/app/components/shadcn/tabs.tsx b/apps/app/components/shadcn/tabs.tsx new file mode 100644 index 00000000..d0db6508 --- /dev/null +++ b/apps/app/components/shadcn/tabs.tsx @@ -0,0 +1,76 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" +import { classNames } from "../utils/classNames" + +function Tabs({ + className, + orientation = "horizontal", + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsList({ + className, + variant = "default", + ...props +}: React.ComponentProps & { variant?: "default" | "line" | "underline" }) { + return ( + + ) +} + +function TabsTrigger({ + className, + variant = "default", + ...props +}: React.ComponentProps & { variant?: "default" | "underline" }) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/apps/app/hooks/usePasskeyLoginFlow.ts b/apps/app/hooks/usePasskeyLoginFlow.ts new file mode 100644 index 00000000..d4434e42 --- /dev/null +++ b/apps/app/hooks/usePasskeyLoginFlow.ts @@ -0,0 +1,78 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { useSecretDerivation } from '@/context/secretDerivationContext' +import { usePasskeyCredentialIds } from '@/stores/secretDerivationStore' +import { useSteps } from '@/hooks/useSteps' +import { mapPasskeyError } from '@/lib/htlc/secretDerivation/passkeyService' + +export type LoginStep = 'unsupported' | 'passkey_recovery' | 'signing' + +interface UsePasskeyLoginFlowOptions { + isActive: boolean + onSuccess: () => void + onDismiss: () => void +} + +export function usePasskeyLoginFlow({ isActive, onSuccess, onDismiss }: UsePasskeyLoginFlowOptions) { + const { loginWithPasskey, derivationMessage, prfSupportDetails, isReady } = useSecretDerivation() + const storedPasskeyIds = usePasskeyCredentialIds() + const hasStoredPasskeys = storedPasskeyIds.length > 0 + const { currentStep, goToStep, canGoBack, reset } = useSteps({ initial: 'signing' }) + const [passkeyError, setPasskeyError] = useState(null) + const [signingError, setSigningError] = useState(null) + const loginTriggered = useRef(false) + const passkeyUnsupported = isReady && prfSupportDetails && !prfSupportDetails.supported + + const startPasskeyLogin = useCallback(async (options?: { forceCreate?: boolean; crossDevice?: boolean }) => { + goToStep('signing') + setPasskeyError(null) + try { + if (options?.forceCreate) { + await loginWithPasskey({ forceCreate: true, label: 'Train' }) + } else if (options?.crossDevice) { + await loginWithPasskey({ crossDevice: true }) + } else { + await loginWithPasskey() + } + onSuccess() + } catch (e) { + const message = mapPasskeyError(e) + setPasskeyError(message) + goToStep('passkey_recovery', 'back') + } + }, [goToStep, loginWithPasskey, onSuccess]) + + useEffect(() => { + if (isActive) { + reset() + setPasskeyError(null) + setSigningError(null) + loginTriggered.current = false + } + }, [isActive, reset]) + + useEffect(() => { + if (isActive && isReady && !loginTriggered.current) { + loginTriggered.current = true + if (passkeyUnsupported) { + goToStep('unsupported') + } else { + startPasskeyLogin(hasStoredPasskeys ? {} : { forceCreate: true }) + } + } + }, [isActive, isReady]) + + const handleBack = useCallback(() => { + onDismiss() + }, [onDismiss]) + + return { + currentStep, + canGoBack, + passkeyError, + signingError, + derivationMessage, + hasStoredPasskeys, + startPasskeyLogin, + handleBack, + } +} diff --git a/apps/app/package.json b/apps/app/package.json index 8295e49a..36a32e5f 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -42,6 +42,7 @@ "@radix-ui/react-progress": "^1.0.2", "@radix-ui/react-select": "^1.2.1", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.0.7", "@solana/spl-token": "catalog:", "@solana/wallet-adapter-base": "^0.9.27", diff --git a/apps/app/stores/loginModalStore.ts b/apps/app/stores/loginModalStore.ts index 6ee7a314..0acd8869 100644 --- a/apps/app/stores/loginModalStore.ts +++ b/apps/app/stores/loginModalStore.ts @@ -1,13 +1,17 @@ import { create } from 'zustand' +type LoginTarget = 'modal' | 'sidebar' + interface LoginModalState { isOpen: boolean - open: () => void + target: LoginTarget | null + open: (target?: LoginTarget | unknown) => void close: () => void } export const useLoginModalStore = create()((set) => ({ isOpen: false, - open: () => set({ isOpen: true }), - close: () => set({ isOpen: false }), + target: null, + open: (target) => set({ isOpen: true, target: (typeof target === 'string' ? target : 'modal') as LoginTarget }), + close: () => set({ isOpen: false, target: null }), })) diff --git a/apps/app/tailwind.config.js b/apps/app/tailwind.config.js index 26d15e24..175b46f4 100644 --- a/apps/app/tailwind.config.js +++ b/apps/app/tailwind.config.js @@ -226,7 +226,7 @@ export default { boxShadow: { 'widget-footer': '-1px -28px 21px -6px var(--ls-colors-secondary-700, #181717)', 'card': '5px 5px 40px rgba(0, 0, 0, 0.2), 0px 0px 20px rgba(0, 0, 0, 0.43)', - 'accordion-open': '0 8px 32px rgba(0, 0, 0, 0.5), 0 4px 16px rgba(0, 0, 0, 0.3)', + 'accordion-open': '0 6px 16px -4px rgba(0, 0, 0, 0.3)', }, typography: (theme) => ({ DEFAULT: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 196bbbdb..cc014e7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.2.4(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tooltip': specifier: ^1.0.7 version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3486,6 +3489,19 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-select@1.2.2': resolution: {integrity: sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==} peerDependencies: @@ -3540,6 +3556,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tooltip@1.2.8': resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} peerDependencies: @@ -15747,6 +15776,23 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@radix-ui/react-select@1.2.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.6 @@ -15812,6 +15858,22 @@ snapshots: optionalDependencies: '@types/react': 18.3.28 + '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 From ceae4c574926532025d22dd31ffdfc737e8535a6 Mon Sep 17 00:00:00 2001 From: yasha-meursault Date: Wed, 1 Apr 2026 20:52:11 +0400 Subject: [PATCH 06/19] minor fixes --- .../app/components/Settings/SettingsModal.tsx | 105 +++++++++--------- apps/app/components/SwapHistory/index.tsx | 22 ++-- apps/app/components/Wizard/WizardItem.tsx | 19 +++- apps/app/components/globalFooter.tsx | 10 +- apps/app/components/navbar.tsx | 5 +- apps/app/components/shadcn/sidebar.tsx | 4 +- 6 files changed, 98 insertions(+), 67 deletions(-) diff --git a/apps/app/components/Settings/SettingsModal.tsx b/apps/app/components/Settings/SettingsModal.tsx index f36503d9..72623f5f 100644 --- a/apps/app/components/Settings/SettingsModal.tsx +++ b/apps/app/components/Settings/SettingsModal.tsx @@ -1,4 +1,4 @@ -import { FC, useEffect, useRef } from "react" +import { Dispatch, FC, ReactNode, SetStateAction, useEffect, useRef, useState } from "react" import VaulDrawer from "@/components/Modal/vaulModal" import { FormWizardProvider, useFormWizardaUpdate, useFormWizardState } from "@/context/formWizardProvider" import { MenuStep } from "@/Models/Wizard" @@ -8,30 +8,31 @@ import NetworkRpcEditView from "@/components/Settings/NetworkRpcEditView" import { useMenuNavigation } from "@/hooks/useMenuNavigation" import { useSwapPreferencesStore } from "@/stores/swapPreferencesStore" import { useTheme } from "next-themes" -import { AnimatePresence } from "framer-motion" import { ChevronLeft, Settings2, Zap, Sun } from "lucide-react" import Menu from "@/components/TrainMenu/Menu" const SettingsModal: FC<{ show: boolean; setShow: (open: boolean) => void }> = ({ show, setShow }) => { + const [headerContent, setHeaderContent] = useState("Settings") + return ( - + ) } -const SettingsModalInner: FC = () => { - const { goBack, currentStepName, moving, wrapperWidth } = useFormWizardState() +const SettingsModalInner: FC<{ setHeaderContent: Dispatch> }> = ({ setHeaderContent }) => { + const { goBack, currentStepName } = useFormWizardState() const { setWrapperWidth, goToStep } = useFormWizardaUpdate() const wrapperRef = useRef(null) @@ -61,10 +62,12 @@ const SettingsModalInner: FC = () => { } }, []) - return ( -
      - {currentStepName !== MenuStep.Menu && ( -
      + useEffect(() => { + if (currentStepName === MenuStep.Menu) { + setHeaderContent("Settings") + } else { + setHeaderContent( +
      -

      {currentStepName as string}

      + {currentStepName as string}
      - )} + ) + } + }, [currentStepName, goBack]) + return ( +
      - - - - - } - checked={autoRevealSecret} - onChange={setAutoRevealSecret} - > - Auto Reveal Secret - - } - checked={theme === "light"} - onChange={(checked) => setTheme(checked ? "light" : "default")} - > - Light Mode - - goToStep(MenuStep.RPCConfiguration)} icon={}> - RPC Configuration - - - - - goToStep(MenuStep.Menu, "back")} inModal> - - - goToStep(MenuStep.RPCConfiguration, "back")} inModal> - {selectedNetwork ? ( - - ) : ( -
      Loading...
      - )} -
      -
      + + + + } + checked={autoRevealSecret} + onChange={setAutoRevealSecret} + > + Auto Reveal Secret + + } + checked={theme === "light"} + onChange={(checked) => setTheme(checked ? "light" : "default")} + > + Light Mode + + goToStep(MenuStep.RPCConfiguration)} icon={}> + RPC Configuration + + + + + goToStep(MenuStep.Menu, "back")} inModal disableAnimation> + + + goToStep(MenuStep.RPCConfiguration, "back")} inModal disableAnimation> + {selectedNetwork ? ( + + ) : ( +
      Loading...
      + )} +
      ) diff --git a/apps/app/components/SwapHistory/index.tsx b/apps/app/components/SwapHistory/index.tsx index 023798d4..a83a4af4 100644 --- a/apps/app/components/SwapHistory/index.tsx +++ b/apps/app/components/SwapHistory/index.tsx @@ -1,5 +1,5 @@ import { FC, useEffect, useMemo, useState } from 'react' -import { ChevronUp } from 'lucide-react' +import { ChevronRight, ChevronUp } from 'lucide-react' import { SwapData, useSwapStore } from '@/stores/swapStore' import { useSettingsState } from '@/context/settings' import { HTLCStatus, isTerminalStatus } from '@/Models/HTLCStatus' @@ -226,8 +226,8 @@ const SwapAccordionItem: FC = ({ hashlock, swap, sourceN const EmptyState = () => (
      - - + +

      @@ -241,13 +241,18 @@ const EmptyState = () => ( ) const SkeletonCard = ({ className }: { className?: string }) => ( -
      -
      +
      +
      -
      +
      -
      -
      +
      +
      +
      +
      +
      +
      +
      @@ -258,7 +263,6 @@ const SkeletonCard = ({ className }: { className?: string }) => (
      -
      ) diff --git a/apps/app/components/Wizard/WizardItem.tsx b/apps/app/components/Wizard/WizardItem.tsx index 3428ca84..f4b7c38d 100644 --- a/apps/app/components/Wizard/WizardItem.tsx +++ b/apps/app/components/Wizard/WizardItem.tsx @@ -11,9 +11,10 @@ type Props = { fitHeight?: boolean, className?: string; inModal?: boolean; + disableAnimation?: boolean; } -const WizardItem: FC = (({ StepName, children, GoBack, PositionPercent, fitHeight = false, className, inModal }: Props) => { +const WizardItem: FC = (({ StepName, children, GoBack, PositionPercent, fitHeight = false, className, inModal, disableAnimation }: Props) => { const { currentStepName, wrapperWidth, moving } = useFormWizardState() const { setGoBack, setPositionPercent } = useFormWizardaUpdate() const styleConfigs = fitHeight ? { width: `${wrapperWidth}px`, height: '100%' } : { width: `${wrapperWidth}px`, minHeight: inModal ? 'inherit' : '350px', height: '100%' } @@ -25,7 +26,19 @@ const WizardItem: FC = (({ StepName, children, GoBack, PositionPercent, f } }, [currentStepName, StepName]) - return currentStepName === StepName ? + if (currentStepName !== StepName) return null + + if (disableAnimation) { + return ( +
      +
      + {Number(wrapperWidth) > 1 && children} +
      +
      + ) + } + + return ( = (({ StepName, children, GoBack, PositionPercent, f {Number(wrapperWidth) > 1 && children}
      - : null + ) }) let variants = { diff --git a/apps/app/components/globalFooter.tsx b/apps/app/components/globalFooter.tsx index 0cabd5d9..2b9f1654 100644 --- a/apps/app/components/globalFooter.tsx +++ b/apps/app/components/globalFooter.tsx @@ -32,8 +32,16 @@ const GLobalFooter = () => { } return ( -