feat: manage passkey drawer
This commit is contained in:
parent
e36dadf97f
commit
f572659dbc
53
apps/accounts/src/pages/passkeys/ManagePasskeyDrawer.tsx
Normal file
53
apps/accounts/src/pages/passkeys/ManagePasskeyDrawer.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { EnteDrawer } from '@ente/shared/components/EnteDrawer';
|
||||
import { PasskeysContext } from '.';
|
||||
import { Stack } from '@mui/material';
|
||||
import Titlebar from '@ente/shared/components/Titlebar';
|
||||
import { MenuItemGroup } from '@ente/shared/components/Menu/MenuItemGroup';
|
||||
import { EnteMenuItem } from '@ente/shared/components/Menu/EnteMenuItem';
|
||||
import { useContext } from 'react';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import MenuItemDivider from '@ente/shared/components/Menu/MenuItemDivider';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
const ManagePasskeyDrawer = (props: IProps) => {
|
||||
const { setShowPasskeyDrawer } = useContext(PasskeysContext);
|
||||
return (
|
||||
<EnteDrawer
|
||||
anchor="right"
|
||||
open={props.open}
|
||||
onClose={() => {
|
||||
setShowPasskeyDrawer(false);
|
||||
}}>
|
||||
<Stack spacing={'4px'} py={'12px'}>
|
||||
<Titlebar
|
||||
onClose={() => {
|
||||
setShowPasskeyDrawer(false);
|
||||
}}
|
||||
title="Manage Passkey"
|
||||
onRootClose={() => {
|
||||
setShowPasskeyDrawer(false);
|
||||
}}
|
||||
/>
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
onClick={() => {}}
|
||||
startIcon={<EditIcon />}
|
||||
label={'Rename Passkey'}
|
||||
/>
|
||||
<MenuItemDivider />
|
||||
<EnteMenuItem
|
||||
onClick={() => {}}
|
||||
startIcon={<DeleteIcon />}
|
||||
label={'Delete Passkey'}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
</Stack>
|
||||
</EnteDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManagePasskeyDrawer;
|
28
apps/accounts/src/pages/passkeys/PasskeyListItem.tsx
Normal file
28
apps/accounts/src/pages/passkeys/PasskeyListItem.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { EnteMenuItem } from '@ente/shared/components/Menu/EnteMenuItem';
|
||||
import { Passkey } from 'types/passkey';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import { useContext } from 'react';
|
||||
import { PasskeysContext } from '.';
|
||||
|
||||
interface IProps {
|
||||
passkey: Passkey;
|
||||
}
|
||||
|
||||
const PasskeyListItem = (props: IProps) => {
|
||||
const { setSelectedPasskey, setShowPasskeyDrawer } =
|
||||
useContext(PasskeysContext);
|
||||
|
||||
return (
|
||||
<EnteMenuItem
|
||||
onClick={() => {
|
||||
setSelectedPasskey(props.passkey);
|
||||
setShowPasskeyDrawer(true);
|
||||
}}
|
||||
key={props.passkey.id}
|
||||
endIcon={<ChevronRightIcon />}
|
||||
label={props.passkey.friendlyName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasskeyListItem;
|
30
apps/accounts/src/pages/passkeys/PasskeysList.tsx
Normal file
30
apps/accounts/src/pages/passkeys/PasskeysList.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { MenuItemGroup } from '@ente/shared/components/Menu/MenuItemGroup';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getPasskeys } from 'services/passkeysService';
|
||||
import { Passkey } from 'types/passkey';
|
||||
import PasskeyListItem from './PasskeyListItem';
|
||||
|
||||
const PasskeyComponent = () => {
|
||||
const [passkeys, setPasskeys] = useState<Passkey[]>([]);
|
||||
|
||||
const init = async () => {
|
||||
const data = await getPasskeys();
|
||||
setPasskeys(data.passkeys);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItemGroup>
|
||||
{passkeys.map((passkey) => (
|
||||
<PasskeyListItem key={passkey.id} passkey={passkey} />
|
||||
))}
|
||||
</MenuItemGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasskeyComponent;
|
|
@ -7,8 +7,26 @@ import {
|
|||
} from '../../services/passkeysService';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import _sodium from 'libsodium-wrappers';
|
||||
import { Dispatch, SetStateAction, createContext, useState } from 'react';
|
||||
import { Passkey } from 'types/passkey';
|
||||
import PasskeysList from './PasskeysList';
|
||||
import ManagePasskeyDrawer from './ManagePasskeyDrawer';
|
||||
|
||||
export const PasskeysContext = createContext(
|
||||
{} as {
|
||||
selectedPasskey: Passkey | null;
|
||||
setSelectedPasskey: Dispatch<SetStateAction<Passkey | null>>;
|
||||
setShowPasskeyDrawer: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
);
|
||||
|
||||
const Passkeys = () => {
|
||||
const [selectedPasskey, setSelectedPasskey] = useState<Passkey | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const [showPasskeyDrawer, setShowPasskeyDrawer] = useState(false);
|
||||
|
||||
const handleSubmit = async (inputValue: string) => {
|
||||
const response: {
|
||||
options: {
|
||||
|
@ -50,18 +68,29 @@ const Passkeys = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<CenteredFlex>
|
||||
<Box>
|
||||
<SingleInputForm
|
||||
fieldType="text"
|
||||
placeholder="Passkey Name"
|
||||
buttonText="Add Passkey"
|
||||
initialValue={''}
|
||||
blockButton
|
||||
callback={handleSubmit}
|
||||
/>
|
||||
</Box>
|
||||
</CenteredFlex>
|
||||
<PasskeysContext.Provider
|
||||
value={{
|
||||
selectedPasskey,
|
||||
setSelectedPasskey,
|
||||
setShowPasskeyDrawer,
|
||||
}}>
|
||||
<CenteredFlex>
|
||||
<Box>
|
||||
<SingleInputForm
|
||||
fieldType="text"
|
||||
placeholder="Passkey Name"
|
||||
buttonText="Add Passkey"
|
||||
initialValue={''}
|
||||
blockButton
|
||||
callback={handleSubmit}
|
||||
/>
|
||||
<Box>
|
||||
<PasskeysList />
|
||||
</Box>
|
||||
</Box>
|
||||
</CenteredFlex>
|
||||
<ManagePasskeyDrawer open={showPasskeyDrawer} />
|
||||
</PasskeysContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,6 +5,22 @@ import { logError } from '@ente/shared/sentry';
|
|||
import _sodium from 'libsodium-wrappers';
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
export const getPasskeys = async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
const response = await HTTPService.get(
|
||||
`${ENDPOINT}/passkeys`,
|
||||
{},
|
||||
{ 'X-Auth-Token': token }
|
||||
);
|
||||
return await response.data;
|
||||
} catch (e) {
|
||||
logError(e, 'get passkeys failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPasskeyRegistrationOptions = async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
|
6
apps/accounts/src/types/passkey.ts
Normal file
6
apps/accounts/src/types/passkey.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export interface Passkey {
|
||||
id: string;
|
||||
userID: number;
|
||||
friendlyName: string;
|
||||
createdAt: number;
|
||||
}
|
42
packages/shared/components/CaptionedText.tsx
Normal file
42
packages/shared/components/CaptionedText.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { ButtonProps, Typography } from '@mui/material';
|
||||
import { VerticallyCenteredFlex } from '@ente/shared/components/Container';
|
||||
|
||||
interface Iprops {
|
||||
mainText: string;
|
||||
subText?: string;
|
||||
subIcon?: React.ReactNode;
|
||||
color?: ButtonProps['color'];
|
||||
}
|
||||
|
||||
const getSubTextColor = (color: ButtonProps['color']) => {
|
||||
switch (color) {
|
||||
case 'critical':
|
||||
return 'critical.main';
|
||||
default:
|
||||
return 'text.faint';
|
||||
}
|
||||
};
|
||||
|
||||
export const CaptionedText = (props: Iprops) => {
|
||||
return (
|
||||
<VerticallyCenteredFlex gap={'4px'}>
|
||||
<Typography> {props.mainText}</Typography>
|
||||
<Typography variant="small" color={getSubTextColor(props.color)}>
|
||||
{'•'}
|
||||
</Typography>
|
||||
{props.subText ? (
|
||||
<Typography
|
||||
variant="small"
|
||||
color={getSubTextColor(props.color)}>
|
||||
{props.subText}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography
|
||||
variant="small"
|
||||
color={getSubTextColor(props.color)}>
|
||||
{props.subIcon}
|
||||
</Typography>
|
||||
)}
|
||||
</VerticallyCenteredFlex>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import { SwitchProps, Switch } from '@mui/material';
|
||||
import { styled } from '@mui/material';
|
||||
const PublicShareSwitch = styled((props: SwitchProps) => (
|
||||
<Switch
|
||||
focusVisibleClassName=".Mui-focusVisible"
|
||||
disableRipple
|
||||
{...props}
|
||||
/>
|
||||
))(({ theme }) => ({
|
||||
width: 40,
|
||||
height: 24,
|
||||
padding: 0,
|
||||
'& .MuiSwitch-switchBase': {
|
||||
padding: 0,
|
||||
margin: 2,
|
||||
transitionDuration: '300ms',
|
||||
'&.Mui-checked': {
|
||||
transform: 'translateX(16px)',
|
||||
color: '#fff',
|
||||
'& + .MuiSwitch-track': {
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark' ? '#2ECA45' : '#65C466',
|
||||
opacity: 1,
|
||||
border: 0,
|
||||
},
|
||||
'&.Mui-disabled + .MuiSwitch-track': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
'&.Mui-focusVisible .MuiSwitch-thumb': {
|
||||
color: '#33cf4d',
|
||||
border: '6px solid #fff',
|
||||
},
|
||||
'&.Mui-disabled .MuiSwitch-thumb': {
|
||||
color:
|
||||
theme.palette.mode === 'light'
|
||||
? theme.palette.grey[100]
|
||||
: theme.palette.grey[600],
|
||||
},
|
||||
'&.Mui-disabled + .MuiSwitch-track': {
|
||||
opacity: theme.palette.mode === 'light' ? 0.7 : 0.3,
|
||||
},
|
||||
},
|
||||
'& .MuiSwitch-thumb': {
|
||||
boxSizing: 'border-box',
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
'& .MuiSwitch-track': {
|
||||
borderRadius: 22 / 2,
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'light'
|
||||
? '#E9E9EA'
|
||||
: theme.colors.fill.muted,
|
||||
opacity: 1,
|
||||
transition: theme.transitions.create(['background-color'], {
|
||||
duration: 500,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
export default PublicShareSwitch;
|
26
packages/shared/components/Directory/changeOption.tsx
Normal file
26
packages/shared/components/Directory/changeOption.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import OverflowMenu from '@ente/shared/components/OverflowMenu/menu';
|
||||
import { OverflowMenuOption } from '@ente/shared/components/OverflowMenu/option';
|
||||
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
||||
import { t } from 'i18next';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
|
||||
export default function ChangeDirectoryOption({
|
||||
changeExportDirectory: changeDirectory,
|
||||
}) {
|
||||
return (
|
||||
<OverflowMenu
|
||||
triggerButtonProps={{
|
||||
sx: {
|
||||
ml: 1,
|
||||
},
|
||||
}}
|
||||
ariaControls={'export-option'}
|
||||
triggerButtonIcon={<MoreHoriz />}>
|
||||
<OverflowMenuOption
|
||||
onClick={changeDirectory}
|
||||
startIcon={<FolderIcon />}>
|
||||
{t('CHANGE_FOLDER')}
|
||||
</OverflowMenuOption>
|
||||
</OverflowMenu>
|
||||
);
|
||||
}
|
34
packages/shared/components/Directory/index.tsx
Normal file
34
packages/shared/components/Directory/index.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { styled } from '@mui/material/styles';
|
||||
import LinkButton from '@ente/shared/components/LinkButton';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import ElectronAPIs from '@ente/shared/electron';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
|
||||
const DirectoryPathContainer = styled(LinkButton)(
|
||||
({ width }) => `
|
||||
width: ${width}px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
/* Beginning of string */
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
`
|
||||
);
|
||||
|
||||
export const DirectoryPath = ({ width, path }) => {
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
await ElectronAPIs.openDirectory(path);
|
||||
} catch (e) {
|
||||
logError(e, 'openDirectory failed');
|
||||
}
|
||||
};
|
||||
return (
|
||||
<DirectoryPathContainer width={width} onClick={handleClick}>
|
||||
<Tooltip title={path}>
|
||||
<span>{path}</span>
|
||||
</Tooltip>
|
||||
</DirectoryPathContainer>
|
||||
);
|
||||
};
|
10
packages/shared/components/EnteDrawer.tsx
Normal file
10
packages/shared/components/EnteDrawer.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Drawer, styled } from '@mui/material';
|
||||
|
||||
export const EnteDrawer = styled(Drawer)(({ theme }) => ({
|
||||
'& .MuiPaper-root': {
|
||||
maxWidth: '375px',
|
||||
width: '100%',
|
||||
scrollbarWidth: 'thin',
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
}));
|
127
packages/shared/components/Menu/EnteMenuItem.tsx
Normal file
127
packages/shared/components/Menu/EnteMenuItem.tsx
Normal file
|
@ -0,0 +1,127 @@
|
|||
import {
|
||||
MenuItem,
|
||||
ButtonProps,
|
||||
Box,
|
||||
Typography,
|
||||
TypographyProps,
|
||||
} from '@mui/material';
|
||||
import { CaptionedText } from '../CaptionedText';
|
||||
import PublicShareSwitch from '../Collections/CollectionShare/publicShare/switch';
|
||||
import {
|
||||
SpaceBetweenFlex,
|
||||
VerticallyCenteredFlex,
|
||||
} from '@ente/shared/components/Container';
|
||||
import React from 'react';
|
||||
import ChangeDirectoryOption from '../Directory/changeOption';
|
||||
|
||||
interface Iprops {
|
||||
onClick: () => void;
|
||||
color?: ButtonProps['color'];
|
||||
variant?:
|
||||
| 'primary'
|
||||
| 'captioned'
|
||||
| 'toggle'
|
||||
| 'secondary'
|
||||
| 'mini'
|
||||
| 'path';
|
||||
fontWeight?: TypographyProps['fontWeight'];
|
||||
startIcon?: React.ReactNode;
|
||||
endIcon?: React.ReactNode;
|
||||
label?: string;
|
||||
subText?: string;
|
||||
subIcon?: React.ReactNode;
|
||||
checked?: boolean;
|
||||
labelComponent?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
export function EnteMenuItem({
|
||||
onClick,
|
||||
color = 'primary',
|
||||
startIcon,
|
||||
endIcon,
|
||||
label,
|
||||
subText,
|
||||
subIcon,
|
||||
checked,
|
||||
variant = 'primary',
|
||||
fontWeight = 'bold',
|
||||
labelComponent,
|
||||
disabled = false,
|
||||
}: Iprops) {
|
||||
const handleButtonClick = () => {
|
||||
if (variant === 'path' || variant === 'toggle') {
|
||||
return;
|
||||
}
|
||||
onClick();
|
||||
};
|
||||
|
||||
const handleIconClick = () => {
|
||||
if (variant !== 'path' && variant !== 'toggle') {
|
||||
return;
|
||||
}
|
||||
onClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
disabled={disabled}
|
||||
onClick={handleButtonClick}
|
||||
sx={{
|
||||
width: '100%',
|
||||
color: (theme) =>
|
||||
variant !== 'captioned' && theme.palette[color].main,
|
||||
...(variant !== 'secondary' &&
|
||||
variant !== 'mini' && {
|
||||
backgroundColor: (theme) => theme.colors.fill.faint,
|
||||
}),
|
||||
'&:hover': {
|
||||
backgroundColor: (theme) => theme.colors.fill.faintPressed,
|
||||
},
|
||||
'& .MuiSvgIcon-root': {
|
||||
fontSize: '20px',
|
||||
},
|
||||
p: 0,
|
||||
borderRadius: '4px',
|
||||
}}>
|
||||
<SpaceBetweenFlex sx={{ pl: '16px', pr: '12px' }}>
|
||||
<VerticallyCenteredFlex sx={{ py: '14px' }} gap={'10px'}>
|
||||
{startIcon && startIcon}
|
||||
<Box px={'2px'}>
|
||||
{labelComponent ? (
|
||||
labelComponent
|
||||
) : variant === 'captioned' ? (
|
||||
<CaptionedText
|
||||
color={color}
|
||||
mainText={label}
|
||||
subText={subText}
|
||||
subIcon={subIcon}
|
||||
/>
|
||||
) : variant === 'mini' ? (
|
||||
<Typography variant="mini" color="text.muted">
|
||||
{label}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography fontWeight={fontWeight}>
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</VerticallyCenteredFlex>
|
||||
<VerticallyCenteredFlex gap={'4px'}>
|
||||
{endIcon && endIcon}
|
||||
{variant === 'toggle' && (
|
||||
<PublicShareSwitch
|
||||
checked={checked}
|
||||
onClick={handleIconClick}
|
||||
/>
|
||||
)}
|
||||
{variant === 'path' && (
|
||||
<ChangeDirectoryOption
|
||||
changeExportDirectory={handleIconClick}
|
||||
/>
|
||||
)}
|
||||
</VerticallyCenteredFlex>
|
||||
</SpaceBetweenFlex>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
16
packages/shared/components/Menu/MenuItemDivider.tsx
Normal file
16
packages/shared/components/Menu/MenuItemDivider.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Divider } from '@mui/material';
|
||||
interface Iprops {
|
||||
hasIcon?: boolean;
|
||||
}
|
||||
export default function MenuItemDivider({ hasIcon = false }: Iprops) {
|
||||
return (
|
||||
<Divider
|
||||
sx={{
|
||||
'&&&': {
|
||||
my: 0,
|
||||
ml: hasIcon ? '48px' : '16px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
20
packages/shared/components/Menu/MenuItemGroup.tsx
Normal file
20
packages/shared/components/Menu/MenuItemGroup.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { styled } from '@mui/material';
|
||||
|
||||
export const MenuItemGroup = styled('div')(
|
||||
({ theme }) => `
|
||||
& > .MuiMenuItem-root{
|
||||
border-radius: 8px;
|
||||
background-color: transparent;
|
||||
}
|
||||
& > .MuiMenuItem-root:not(:last-of-type) {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
& > .MuiMenuItem-root:not(:first-of-type) {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
background-color: ${theme.colors.fill.faint};
|
||||
border-radius: 8px;
|
||||
`
|
||||
);
|
27
packages/shared/components/Menu/MenuSectionTitle.tsx
Normal file
27
packages/shared/components/Menu/MenuSectionTitle.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Typography } from '@mui/material';
|
||||
import { VerticallyCenteredFlex } from '@ente/shared/components/Container';
|
||||
|
||||
interface Iprops {
|
||||
title: string;
|
||||
icon?: JSX.Element;
|
||||
}
|
||||
|
||||
export default function MenuSectionTitle({ title, icon }: Iprops) {
|
||||
return (
|
||||
<VerticallyCenteredFlex
|
||||
px="8px"
|
||||
py={'6px'}
|
||||
gap={'8px'}
|
||||
sx={{
|
||||
'& > svg': {
|
||||
fontSize: '17px',
|
||||
color: (theme) => theme.colors.stroke.muted,
|
||||
},
|
||||
}}>
|
||||
{icon && icon}
|
||||
<Typography variant="small" color="text.muted">
|
||||
{title}
|
||||
</Typography>
|
||||
</VerticallyCenteredFlex>
|
||||
);
|
||||
}
|
56
packages/shared/components/Titlebar.tsx
Normal file
56
packages/shared/components/Titlebar.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import Close from '@mui/icons-material/Close';
|
||||
import ArrowBack from '@mui/icons-material/ArrowBack';
|
||||
import { Box, IconButton, Typography } from '@mui/material';
|
||||
import { FlexWrapper } from '@ente/shared/components/Container';
|
||||
|
||||
interface Iprops {
|
||||
title: string;
|
||||
caption?: string;
|
||||
onClose: () => void;
|
||||
backIsClose?: boolean;
|
||||
onRootClose?: () => void;
|
||||
actionButton?: JSX.Element;
|
||||
}
|
||||
|
||||
export default function Titlebar({
|
||||
title,
|
||||
caption,
|
||||
onClose,
|
||||
backIsClose,
|
||||
actionButton,
|
||||
onRootClose,
|
||||
}: Iprops): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<FlexWrapper
|
||||
height={48}
|
||||
alignItems={'center'}
|
||||
justifyContent="space-between">
|
||||
<IconButton
|
||||
onClick={onClose}
|
||||
color={backIsClose ? 'secondary' : 'primary'}>
|
||||
{backIsClose ? <Close /> : <ArrowBack />}
|
||||
</IconButton>
|
||||
<Box display={'flex'} gap="4px">
|
||||
{actionButton && actionButton}
|
||||
{!backIsClose && (
|
||||
<IconButton onClick={onRootClose} color={'secondary'}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</FlexWrapper>
|
||||
<Box py={0.5} px={2}>
|
||||
<Typography variant="h3" fontWeight={'bold'}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="small"
|
||||
color="text.muted"
|
||||
sx={{ wordBreak: 'break-all', minHeight: '17px' }}>
|
||||
{caption}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue