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';
|
} from '../../services/passkeysService';
|
||||||
import { logError } from '@ente/shared/sentry';
|
import { logError } from '@ente/shared/sentry';
|
||||||
import _sodium from 'libsodium-wrappers';
|
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 Passkeys = () => {
|
||||||
|
const [selectedPasskey, setSelectedPasskey] = useState<Passkey | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const [showPasskeyDrawer, setShowPasskeyDrawer] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = async (inputValue: string) => {
|
const handleSubmit = async (inputValue: string) => {
|
||||||
const response: {
|
const response: {
|
||||||
options: {
|
options: {
|
||||||
|
@ -50,6 +68,12 @@ const Passkeys = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PasskeysContext.Provider
|
||||||
|
value={{
|
||||||
|
selectedPasskey,
|
||||||
|
setSelectedPasskey,
|
||||||
|
setShowPasskeyDrawer,
|
||||||
|
}}>
|
||||||
<CenteredFlex>
|
<CenteredFlex>
|
||||||
<Box>
|
<Box>
|
||||||
<SingleInputForm
|
<SingleInputForm
|
||||||
|
@ -60,8 +84,13 @@ const Passkeys = () => {
|
||||||
blockButton
|
blockButton
|
||||||
callback={handleSubmit}
|
callback={handleSubmit}
|
||||||
/>
|
/>
|
||||||
|
<Box>
|
||||||
|
<PasskeysList />
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</CenteredFlex>
|
</CenteredFlex>
|
||||||
|
<ManagePasskeyDrawer open={showPasskeyDrawer} />
|
||||||
|
</PasskeysContext.Provider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,22 @@ import { logError } from '@ente/shared/sentry';
|
||||||
import _sodium from 'libsodium-wrappers';
|
import _sodium from 'libsodium-wrappers';
|
||||||
const ENDPOINT = getEndpoint();
|
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 () => {
|
export const getPasskeyRegistrationOptions = async () => {
|
||||||
try {
|
try {
|
||||||
const token = getToken();
|
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