Skip to content

Commit

Permalink
Design tweaks to swap/auction form (#1168)
Browse files Browse the repository at this point in the history
* Make asset/balance selectors grow out of their buttons

* Redesign BalanceValueView and add a tooltip

* Tweak layout of balance/asset selectors

* Use single-row layout

* Reorganize swap form

* Animate the swap UI more

* Support motion in EduInfoCard

* Support motion in InputBlock

* Tighten up syntax

* Support motion in Card and GradientHeader

* Use motion throughout Swap page

* Fix layout issues in AuctionList

* Calculate estimated outputs for an auction

* Refactor to use new helper

* Add changeset

* Undo changes to amount types

* Tweak variable name

* Simplify code

* Fix bug with loading property

* Remove unneeded loading prop

* Add cmoment

* review updates

* Box component css tweaks

---------

Co-authored-by: Gabe Rodriguez <[email protected]>
  • Loading branch information
jessepinho and grod220 authored May 30, 2024
1 parent aabe23c commit 120b654
Show file tree
Hide file tree
Showing 28 changed files with 663 additions and 332 deletions.
7 changes: 7 additions & 0 deletions .changeset/hip-crabs-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@penumbra-zone/getters': minor
'minifront': minor
'@penumbra-zone/ui': minor
---

Support estimates of outputs for auctions; redesign the estimate results part of the swap/auction UI
117 changes: 68 additions & 49 deletions apps/minifront/src/components/shared/balance-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
import { useState } from 'react';
import { useId, useState } from 'react';
import { IconInput } from '@penumbra-zone/ui/components/ui/icon-input';
import {
Dialog,
DialogClose,
DialogContent,
DialogHeader,
DialogTrigger,
} from '@penumbra-zone/ui/components/ui/dialog';
import { cn } from '@penumbra-zone/ui/lib/utils';
import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value';
import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
import { getAddressIndex } from '@penumbra-zone/getters/address-view';
import { getDisplayDenomFromView, getSymbolFromValueView } from '@penumbra-zone/getters/value-view';
import { Box } from '@penumbra-zone/ui/components/ui/box';
import { motion } from 'framer-motion';

const bySearch = (search: string) => (balancesResponse: BalancesResponse) =>
getDisplayDenomFromView(balancesResponse.balanceView)
Expand All @@ -39,60 +39,79 @@ interface BalanceSelectorProps {
*/
export default function BalanceSelector({ value, balances, onChange }: BalanceSelectorProps) {
const [search, setSearch] = useState('');
const [isOpen, setIsOpen] = useState(false);
const filteredBalances = search ? balances.filter(bySearch(search)) : balances;
const layoutId = useId();

return (
<Dialog>
<DialogTrigger disabled={!balances.length}>
<div className='flex h-9 min-w-[100px] max-w-[200px] items-center justify-center gap-2 rounded-lg bg-light-brown px-2'>
<>
{!isOpen && (
<motion.div
layout
layoutId={layoutId}
className='flex min-w-[100px] max-w-[200px] cursor-pointer items-center justify-center rounded-lg bg-light-brown px-2'
onClick={() => setIsOpen(true)}
>
<ValueViewComponent view={value?.balanceView} showValue={false} />
</div>
</DialogTrigger>
<DialogContent>
<div className='flex max-h-screen flex-col'>
<DialogHeader>Select asset</DialogHeader>
<div className='flex shrink flex-col gap-4 overflow-auto p-4'>
<Box spacing='compact'>
<IconInput
icon={<MagnifyingGlassIcon className='size-5 text-muted-foreground' />}
value={search}
onChange={setSearch}
placeholder='Search assets...'
/>
</Box>
<div className='mt-2 grid grid-cols-4 font-headline text-base font-semibold'>
<p className='flex justify-start'>Account</p>
<p className='col-span-3 flex justify-start'>Asset</p>
</div>
<div className='flex flex-col gap-2'>
{filteredBalances.map((b, i) => {
const index = getAddressIndex(b.accountAddress).account;
</motion.div>
)}

{isOpen && (
<>
{/* 0-opacity placeholder for layout's sake */}
<div className='flex min-w-[100px] max-w-[200px] px-2 opacity-0'>
<ValueViewComponent view={value?.balanceView} showValue={false} />
</div>
</>
)}

<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent layoutId={layoutId}>
<div className='flex max-h-screen flex-col'>
<DialogHeader>Select asset</DialogHeader>
<div className='flex shrink flex-col gap-4 overflow-auto p-4'>
<Box spacing='compact'>
<IconInput
icon={<MagnifyingGlassIcon className='size-5 text-muted-foreground' />}
value={search}
onChange={setSearch}
placeholder='Search assets...'
/>
</Box>
<div className='mt-2 grid grid-cols-4 font-headline text-base font-semibold'>
<p className='flex justify-start'>Account</p>
<p className='col-span-3 flex justify-start'>Asset</p>
</div>
<div className='flex flex-col gap-2'>
{filteredBalances.map((b, i) => {
const index = getAddressIndex(b.accountAddress).account;

return (
<div key={i} className='flex flex-col'>
<DialogClose>
<div
className={cn(
'grid grid-cols-4 py-[10px] cursor-pointer hover:bg-light-brown hover:px-4 hover:-mx-4 font-bold text-muted-foreground',
value?.balanceView?.equals(b.balanceView) &&
value.accountAddress?.equals(b.accountAddress) &&
'bg-light-brown px-4 -mx-4',
)}
onClick={() => onChange(b)}
>
<p className='flex justify-start'>{index}</p>
<div className='col-span-3 flex justify-start'>
<ValueViewComponent view={b.balanceView} />
return (
<div key={i} className='flex flex-col'>
<DialogClose>
<div
className={cn(
'grid grid-cols-4 py-[10px] cursor-pointer hover:bg-light-brown hover:px-4 hover:-mx-4 font-bold text-muted-foreground',
value?.balanceView?.equals(b.balanceView) &&
value.accountAddress?.equals(b.accountAddress) &&
'bg-light-brown px-4 -mx-4',
)}
onClick={() => onChange(b)}
>
<p className='flex justify-start'>{index}</p>
<div className='col-span-3 flex justify-start'>
<ValueViewComponent view={b.balanceView} />
</div>
</div>
</div>
</DialogClose>
</div>
);
})}
</DialogClose>
</div>
);
})}
</div>
</div>
</div>
</div>
</DialogContent>
</Dialog>
</DialogContent>
</Dialog>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@ import { Card } from '@penumbra-zone/ui/components/ui/card';
import { cn } from '@penumbra-zone/ui/lib/utils';
import { GradientHeader } from '@penumbra-zone/ui/components/ui/gradient-header';
import { EduPanel, eduPanelContent } from './content';
import { motion } from 'framer-motion';

interface HelperCardProps {
src: string;
label: string;
className?: string;
content: EduPanel;
layout?: boolean;
}

export const EduInfoCard = ({ src, label, className, content }: HelperCardProps) => {
export const EduInfoCard = ({ src, label, className, content, layout }: HelperCardProps) => {
return (
<Card gradient className={cn('p-5 row-span-1', className)}>
<div className='flex gap-2'>
<Card gradient className={cn('p-5 row-span-1', className)} layout={layout}>
<motion.div layout={layout} className='flex gap-2'>
<img src={src} alt='icons' className='size-[30px] md:size-8' />
<GradientHeader>{label}</GradientHeader>
</div>
</motion.div>
<p className='mt-4 text-muted-foreground md:mt-2'>{eduPanelContent[content]}</p>
</Card>
);
Expand Down
26 changes: 19 additions & 7 deletions apps/minifront/src/components/shared/input-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,30 @@ interface InputBlockProps {
validations?: Validation[] | undefined;
value?: unknown;
children: ReactNode;
layout?: boolean;
layoutId?: string;
}

export const InputBlock = ({ label, className, validations, value, children }: InputBlockProps) => {
export const InputBlock = ({
label,
className,
validations,
value,
children,
layout,
layoutId,
}: InputBlockProps) => {
const vResult = typeof value === 'string' ? validationResult(value, validations) : undefined;

return (
<Box>
<Box
label={label}
headerContent={
vResult ? <div className={cn('italic', 'text-red-400')}>{vResult.issue}</div> : null
}
layout={layout}
layoutId={layoutId}
>
<div
className={cn(
'flex flex-col gap-1',
Expand All @@ -24,11 +41,6 @@ export const InputBlock = ({ label, className, validations, value, children }: I
className,
)}
>
<div className='flex items-center gap-2'>
<p className='text-base font-bold'>{label}</p>
{vResult ? <div className={cn('italic', 'text-red-400')}>{vResult.issue}</div> : null}
</div>

{children}
</div>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const FormDialog = ({
{!!open && !!action && (
<>
<DialogHeader>{getCapitalizedAction(action)}</DialogHeader>
<form className='flex flex-col gap-4 overflow-hidden px-4 pb-4' onSubmit={handleSubmit}>
<form className='flex flex-col gap-4 overflow-hidden p-4' onSubmit={handleSubmit}>
<div className='flex flex-col'>
<div className='truncate'>{validator.name}</div>
<IdentityKeyComponent identityKey={getIdentityKey(validator)} />
Expand Down
12 changes: 6 additions & 6 deletions apps/minifront/src/components/swap/auction-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { bech32mAuctionId } from '@penumbra-zone/bech32m/pauctid';
import { SegmentedPicker } from '@penumbra-zone/ui/components/ui/segmented-picker';
import { useMemo } from 'react';
import { getFilteredAuctionInfos } from './get-filtered-auction-infos';
import { LayoutGroup } from 'framer-motion';
import { LayoutGroup, motion } from 'framer-motion';
import { SORT_FUNCTIONS, getMetadata } from './helpers';

const auctionListSelector = (state: AllSlices) => ({
Expand Down Expand Up @@ -58,12 +58,12 @@ export const AuctionList = () => {
);

return (
<Card>
<Card layout>
<div className='mb-4 flex items-center justify-between'>
<GradientHeader>My Auctions</GradientHeader>
<GradientHeader layout>My Auctions</GradientHeader>

<div className='flex items-center gap-2'>
{!!filteredAuctionInfos.length && <QueryLatestStateButton />}
<motion.div layout className='flex items-center gap-2'>
{!!auctionInfos.length && <QueryLatestStateButton />}

<SegmentedPicker
value={filter}
Expand All @@ -74,7 +74,7 @@ export const AuctionList = () => {
{ label: 'All', value: 'all' },
]}
/>
</div>
</motion.div>
</div>

<div className='flex flex-col gap-2'>
Expand Down
21 changes: 12 additions & 9 deletions apps/minifront/src/components/swap/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,26 @@ import { SwapForm } from './swap-form';
import { UnclaimedSwaps } from './unclaimed-swaps';
import { AuctionList } from './auction-list';
import { SwapInfoCard } from './swap-info-card';
import { LayoutGroup } from 'framer-motion';

export const SwapLayout = () => {
return (
<RestrictMaxWidth>
<div className='grid w-full grid-std-spacing md:grid-cols-3'>
<div className='flex flex-col overflow-hidden grid-std-spacing md:col-span-2'>
<SwapForm />
<LayoutGroup>
<div className='grid w-full grid-std-spacing md:grid-cols-3'>
<div className='flex flex-col overflow-hidden grid-std-spacing md:col-span-2'>
<SwapForm />

<AuctionList />
</div>
<AuctionList />
</div>

<div className='flex flex-col grid-std-spacing'>
<SwapInfoCard />
<div className='flex flex-col grid-std-spacing'>
<SwapInfoCard />

<UnclaimedSwaps />
<UnclaimedSwaps />
</div>
</div>
</div>
</LayoutGroup>
</RestrictMaxWidth>
);
};
40 changes: 40 additions & 0 deletions apps/minifront/src/components/swap/swap-form/estimate-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { buttonVariants } from '@penumbra-zone/ui/components/ui/button';
import {
Tooltip,
TooltipProvider,
TooltipTrigger,
TooltipContent,
} from '@penumbra-zone/ui/components/ui/tooltip';
import { cn } from '@penumbra-zone/ui/lib/utils';

export const EstimateButton = ({
disabled,
onClick,
}: {
disabled: boolean;
onClick: () => void;
}) => {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
// Style as a button
className={cn('w-full', buttonVariants({ variant: 'secondary', size: 'sm' }))}
onClick={e => {
e.preventDefault();
onClick();
}}
disabled={disabled}
>
Estimate
</TooltipTrigger>
<TooltipContent side='bottom' className='w-60'>
<p>
Privacy note: This makes a request to your config&apos;s gRPC node to simulate a swap of
these assets. That means you are possibly revealing your intent to this node.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
Loading

0 comments on commit 120b654

Please sign in to comment.