1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 04:46:40 +07:00

added node edit menu

This commit is contained in:
Fedor Katurov 2022-07-15 12:13:27 +07:00
parent f185914c7c
commit 74f4c7562b
16 changed files with 320 additions and 175 deletions

View file

@ -47,7 +47,7 @@ const CommentAvatar: FC<Props> = ({ user, withDetails, className }) => {
</Reference> </Reference>
{hovered && withDetails && ( {hovered && withDetails && (
<Popper placement="right" modifiers={modifiers}> <Popper placement="right" modifiers={modifiers} strategy="fixed">
{({ style, ref }) => ( {({ style, ref }) => (
<div style={style} ref={ref} className={styles.popper}> <div style={style} ref={ref} className={styles.popper}>
<h4 className={styles.username}>{user.fullname || user.username}</h4> <h4 className={styles.username}>{user.fullname || user.username}</h4>

View file

@ -11,11 +11,11 @@
background-color: darken($content_bg, 4%); background-color: darken($content_bg, 4%);
padding: $gap; padding: $gap;
box-sizing:border-box; box-sizing:border-box;
z-index: 4;
touch-action: none; touch-action: none;
pointer-events: none; pointer-events: none;
border-radius: $radius; border-radius: $radius;
animation: appear forwards 250ms; animation: appear forwards 250ms;
z-index: 100;
} }
.username { .username {

View file

@ -0,0 +1,64 @@
import React, { FC, ReactNode } from 'react';
import classNames from 'classnames';
import { Manager, Popper, Reference } from 'react-popper';
import { Icon } from '~/components/input/Icon';
import { useFocusEvent } from '~/hooks/dom/useFocusEvent';
import styles from './styles.module.scss';
interface MenuButtonProps {
icon?: ReactNode;
className?: string;
}
const modifiers = [
{
name: 'offset',
options: {
offset: [5, 10],
},
},
];
const MenuButton: FC<MenuButtonProps> = ({
children,
className,
icon = <Icon icon="dots-vertical" size={24} />,
}) => {
const { focused, onFocus, onBlur } = useFocusEvent(false, 150);
return (
<Manager>
<Reference>
{({ ref }) => (
<button
className={classNames(styles.menu, className)}
ref={ref}
onFocus={onFocus}
onBlur={onBlur}
>
{icon}
</button>
)}
</Reference>
{focused && (
<Popper placement="bottom-end" modifiers={modifiers}>
{({ style, ref, placement }) => (
<div
style={style}
ref={ref}
className={classNames(styles.popper, { [styles.top]: placement === 'top-end' })}
>
{children}
</div>
)}
</Popper>
)}
</Manager>
);
};
export { MenuButton };

View file

@ -0,0 +1,40 @@
.menu {
position: relative;
cursor: pointer;
}
@import "src/styles/variables.scss";
@keyframes appear {
0% { opacity: 0 }
100% { opacity: 1 }
}
.popper {
@include outer_shadow;
background-color: $menu_bg;
box-sizing: border-box;
z-index: 12;
border-radius: $radius;
animation: appear forwards 250ms;
&::after {
content: ' ';
width: 0;
height: 0;
border-style: solid;
border-width: 0 10px 10px 10px;
border-color: transparent transparent lighten($menu_bg, 6%) transparent;
position: absolute;
top: -11px;
right: 10px;
}
&.top::after {
border-width: 10px 10px 0 10px;
border-color: darken($menu_bg, 8%) transparent transparent transparent;
top: auto;
bottom: -11px;
}
}

View file

@ -0,0 +1,23 @@
import React, { FC } from 'react';
import { Icon } from '~/components/input/Icon';
import styles from './styles.module.scss';
interface MenuItemWithIconProps {
children: string;
icon: string;
onClick?: () => void;
}
const MenuItemWithIcon: FC<MenuItemWithIconProps> = ({ children, icon, onClick }) => (
<button className={styles.item} onClick={onClick}>
<div className={styles.icon}>
<Icon icon={icon} size={20} />
</div>
<div className={styles.text}>{children}</div>
</button>
);
export { MenuItemWithIcon };

View file

@ -0,0 +1,29 @@
@import "src/styles/variables";
.item {
@include row_shadow;
@include hover_opacity;
font: $font_14_medium;
line-height: 20px;
padding: $gap + 2px $gap $gap - 2px;
display: flex;
flex-direction: row;
color: white;
align-items: stretch;
justify-content: center;
width: 100%;
cursor: pointer;
}
.icon {
flex: 0 0 20px;
margin-right: $gap;
}
.text {
flex: 1;
text-align: left;
padding-right: $gap;
white-space: nowrap;
}

View file

@ -0,0 +1,31 @@
import React, { FC, ReactNode, useMemo } from 'react';
import classNames from 'classnames';
import styles from './styles.module.scss';
interface SeparatedMenuProps {
className?: string;
}
const SeparatedMenu: FC<SeparatedMenuProps> = ({ children, className }) => {
const items = useMemo<ReactNode[]>(() => {
if (!children) {
return [];
}
return (Array.isArray(children) ? children : [children]).filter(it => it);
}, [children]);
return (
<div className={classNames(styles.menu, className)}>
{items.map((item, index) => (
<div className={styles.item} key={index}>
{item}
</div>
))}
</div>
);
};
export { SeparatedMenu };

View file

@ -0,0 +1,29 @@
@import "src/styles/mixins";
@import "src/styles/variables";
.menu {
flex: 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.item {
margin-left: $gap * 4;
position: relative;
&:not(:last-child)::after {
@include inner_shadow;
content: ' ';
position: absolute;
width: 3px;
height: 16px;
background: darken($content_bg, 1%);
display: flex;
top: 5px;
right: -$gap * 2 - 2px;
border-radius: 2px;
}
}

View file

@ -0,0 +1,5 @@
export * from './VerticalMenu';
export * from './HorizontalMenu';
export * from './MenuButton';
export * from './MenuItemWithIcon';
export * from './SeparatedMenu';

View file

@ -1,12 +1,17 @@
import React, { VFC } from 'react'; import React, { VFC } from 'react';
import Tippy from '@tippyjs/react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Icon } from '~/components/input/Icon'; import { Icon } from '~/components/input/Icon';
import { MenuButton, MenuItemWithIcon, SeparatedMenu } from '~/components/menu';
import { useWindowSize } from '~/hooks/dom/useWindowSize';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
interface NodeEditMenuProps { interface NodeEditMenuProps {
className?: string;
canStar: boolean; canStar: boolean;
isHeroic: boolean; isHeroic: boolean;
@ -18,36 +23,62 @@ interface NodeEditMenuProps {
} }
const NodeEditMenu: VFC<NodeEditMenuProps> = ({ const NodeEditMenu: VFC<NodeEditMenuProps> = ({
className,
canStar, canStar,
isHeroic, isHeroic,
isLocked, isLocked,
onStar, onStar,
onLock, onLock,
onEdit, onEdit,
}) => ( }) => {
<div className={styles.editor_menu}> const { isMobile } = useWindowSize();
<div className={styles.editor_menu_button}>
<Icon icon="dots-vertical" size={24} />
</div>
<div className={styles.editor_buttons}> if (isMobile) {
return (
<MenuButton
icon={<Icon icon="dots-vertical" className={styles.icon} size={24} />}
className={className}
>
{canStar && ( {canStar && (
<div className={classNames(styles.star, { [styles.is_heroic]: isHeroic })}> <MenuItemWithIcon icon={isHeroic ? 'star_full' : 'star'} onClick={onStar}>
{isHeroic ? ( {isHeroic ? 'Убрать с главной' : 'На главную'}
<Icon icon="star_full" size={24} onClick={onStar} /> </MenuItemWithIcon>
) : (
<Icon icon="star" size={24} onClick={onStar} />
)}
</div>
)} )}
<div> <MenuItemWithIcon icon="edit" onClick={onEdit}>
<Icon icon={isLocked ? 'locked' : 'unlocked'} size={24} onClick={onLock} /> Редактировать
</div> </MenuItemWithIcon>
<Icon icon="edit" size={24} onClick={onEdit} /> <MenuItemWithIcon icon={isLocked ? 'locked' : 'unlocked'} onClick={onLock}>
</div> {isLocked ? 'Восстановить' : 'Удалить'}
</div> </MenuItemWithIcon>
</MenuButton>
); );
}
return (
<SeparatedMenu>
{canStar && (
<Tippy content={isHeroic ? 'Убрать с главной' : 'На главную'}>
<button className={className} onClick={onStar}>
<Icon icon={isHeroic ? 'star_full' : 'star'} size={24} />
</button>
</Tippy>
)}
<Tippy content="Редактировать">
<button className={className} onClick={onEdit}>
<Icon icon="edit" size={24} />
</button>
</Tippy>
<Tippy content={isLocked ? 'Восстановить' : 'Удалить'}>
<button className={className} onClick={onLock}>
<Icon icon={isLocked ? 'locked' : 'unlocked'} size={24} />
</button>
</Tippy>
</SeparatedMenu>
);
};
export { NodeEditMenu }; export { NodeEditMenu };

View file

@ -1,109 +1,3 @@
@import "src/styles/variables"; .icon {
@import "src/styles/mixins"; fill: currentColor;
@mixin button {
margin: 12px $gap 0 $gap;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
svg {
fill: darken(white, 50%);
transition: fill 0.25s;
}
&:hover {
svg {
fill: $red;
}
}
&::after {
content: ' ';
flex: 0 0 6px;
height: $gap;
width: 6px;
border-radius: 4px;
background: transparentize(black, 0.7);
margin-left: $gap * 2;
}
}
.editor_buttons {
flex: 0;
padding-right: $gap;
fill: transparentize(white, 0.7);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
& > * {
@include button;
}
@include tablet {
align-self: center;
display: none;
& > * {
&:last-child {
margin-right: 0;
&::after {
display: none;
}
}
&:first-child {
margin-left: 0;
}
}
}
}
.star {
transition: fill, stroke 0.25s;
will-change: transform;
.is_heroic {
svg {
fill: $orange;
}
}
&:hover {
fill: $orange;
}
}
.editor_menu_button {
display: none !important;
@include button();
@include tablet {
display: flex !important;
}
}
.editor_menu {
&:hover {
.editor_buttons {
@include tablet {
display: flex;
position: absolute;
right: 0;
top: 100%;
background: darken($content_bg, 4%);
padding: $gap * 2;
border-radius: $radius;
box-shadow: transparentize(black, 0.8) 5px 5px 5px;
transform: translate(0, -10px);
z-index: 10;
}
}
}
} }

View file

@ -3,6 +3,7 @@ import React, { memo, VFC } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Icon } from '~/components/input/Icon'; import { Icon } from '~/components/input/Icon';
import { SeparatedMenu } from '~/components/menu';
import { NodeEditMenu } from '~/components/node/NodeEditMenu'; import { NodeEditMenu } from '~/components/node/NodeEditMenu';
import { Placeholder } from '~/components/placeholders/Placeholder'; import { Placeholder } from '~/components/placeholders/Placeholder';
import { getPrettyDate } from '~/utils/dom'; import { getPrettyDate } from '~/utils/dom';
@ -74,8 +75,10 @@ const NodeTitle: VFC<IProps> = memo(
)} )}
</div> </div>
<SeparatedMenu className={styles.buttons}>
{canEdit && ( {canEdit && (
<NodeEditMenu <NodeEditMenu
className={styles.button}
canStar={canStar} canStar={canStar}
isHeroic={isHeroic} isHeroic={isHeroic}
isLocked={isLocked} isLocked={isLocked}
@ -85,9 +88,10 @@ const NodeTitle: VFC<IProps> = memo(
/> />
)} )}
<div className={styles.buttons}>
{canLike && ( {canLike && (
<div className={classNames(styles.like, { [styles.is_liked]: isLiked })}> <div
className={classNames(styles.button, styles.like, { [styles.is_liked]: isLiked })}
>
{isLiked ? ( {isLiked ? (
<Icon icon="heart_full" size={24} onClick={onLike} /> <Icon icon="heart_full" size={24} onClick={onLike} />
) : ( ) : (
@ -99,7 +103,7 @@ const NodeTitle: VFC<IProps> = memo(
)} )}
</div> </div>
)} )}
</div> </SeparatedMenu>
</div> </div>
</div> </div>
); );

View file

@ -72,7 +72,7 @@
} }
.name { .name {
font: $font_14_regular; font: $font_12_regular;
color: transparentize(white, 0.5); color: transparentize(white, 0.5);
text-transform: lowercase; text-transform: lowercase;
@ -95,37 +95,6 @@
min-width: 0; min-width: 0;
} }
.buttons {
flex: 0;
padding-right: $gap;
fill: transparentize(white, 0.7);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-top: 12px;
& > * {
@include button;
}
@include tablet {
align-self: center;
}
}
.buttons {
& > * {
&:last-child {
margin-right: 0;
&::after {
display: none;
}
}
}
}
.mark { .mark {
flex: 0 0 32px; flex: 0 0 32px;
position: relative; position: relative;
@ -177,8 +146,11 @@
will-change: transform; will-change: transform;
position: relative; position: relative;
flex: 0 0 32px; flex: 0 0 32px;
fill: currentColor;
&.is_liked { &.is_liked {
opacity: 1;
svg { svg {
fill: $red; fill: $red;
} }
@ -213,3 +185,13 @@
pointer-events: none; pointer-events: none;
touch-action: none; touch-action: none;
} }
.buttons {
margin-top: 12px;
margin-right: $gap;
}
.button {
color: white;
@include hover_opacity;
}

View file

@ -67,3 +67,4 @@ $side_pane_btn_color: lighten($main_bg_color, 0%);
$node_buttons_bg: darken($main_bg_color, 6%); $node_buttons_bg: darken($main_bg_color, 6%);
$tag_bg: lighten($main_bg_color, 4%); $tag_bg: lighten($main_bg_color, 4%);
$menu_bg: lighten($main_bg_color, 4%);

View file

@ -274,3 +274,12 @@
} }
} }
} }
@mixin hover_opacity($initial_opacity: 0.5) {
opacity: $initial_opacity;
transition: opacity 0.25s;
&:hover {
opacity: 1;
}
}

View file

@ -46,3 +46,6 @@ table {
border-collapse: collapse; border-collapse: collapse;
border-spacing: 0; border-spacing: 0;
} }
button {
padding: 0;
}