From 5fef4bc80411873f8948abfe7811a8c55bb17360 Mon Sep 17 00:00:00 2001
From: Fedor Katurov <gotham48@gmail.com>
Date: Wed, 13 Oct 2021 16:14:37 +0700
Subject: [PATCH] added flow display controls overlay

---
 .env.development                              |  4 +-
 src/components/common/MenuDots/index.tsx      | 17 ++++
 src/components/flow/FlowCell/index.tsx        | 26 ++++-
 .../flow/FlowCell/styles.module.scss          | 48 ++++++++++
 src/components/flow/FlowCellMenu/index.tsx    | 79 +++++++--------
 .../flow/FlowCellMenu/styles.module.scss      | 81 ++++++++--------
 src/components/flow/FlowGrid/index.tsx        |  2 +-
 src/components/input/Toggle/index.tsx         | 17 +++-
 .../input/Toggle/styles.module.scss           | 18 +++-
 src/sprites/Sprites.tsx                       | 95 ++++++++++++++++---
 src/styles/variables.scss                     |  9 ++
 src/utils/hooks/flow/useFlowCellControls.ts   | 12 +--
 src/utils/hooks/useClickOutsideFocus.ts       | 30 ++++++
 src/utils/types.ts                            |  5 +
 14 files changed, 327 insertions(+), 116 deletions(-)
 create mode 100644 src/components/common/MenuDots/index.tsx
 create mode 100644 src/utils/hooks/useClickOutsideFocus.ts

diff --git a/.env.development b/.env.development
index 589f28a1..cf60c666 100644
--- a/.env.development
+++ b/.env.development
@@ -1,3 +1,3 @@
 #REACT_APP_API_HOST=http://localhost:3334/
-REACT_APP_API_HOST=https://pig.vault48.org/
-REACT_APP_REMOTE_CURRENT=https://pig.vault48.org/static/
+REACT_APP_API_HOST=https://pig.staging.vault48.org/
+REACT_APP_REMOTE_CURRENT=https://pig.staging.vault48.org/static/
diff --git a/src/components/common/MenuDots/index.tsx b/src/components/common/MenuDots/index.tsx
new file mode 100644
index 00000000..09671742
--- /dev/null
+++ b/src/components/common/MenuDots/index.tsx
@@ -0,0 +1,17 @@
+import React, { FC } from 'react';
+import styles from '~/components/flow/FlowCell/styles.module.scss';
+import { Icon } from '~/components/input/Icon';
+import { ButtonProps } from '~/utils/types';
+import classNames from 'classnames';
+
+interface Props extends ButtonProps {}
+
+const MenuDots: FC<Props> = ({ ...rest }) => (
+  <button {...rest} className={classNames(styles.button, rest.className)}>
+    <div className={styles.dots}>
+      <Icon icon="menu" size={24} />
+    </div>
+  </button>
+);
+
+export { MenuDots };
diff --git a/src/components/flow/FlowCell/index.tsx b/src/components/flow/FlowCell/index.tsx
index 8e908ee1..107b21e8 100644
--- a/src/components/flow/FlowCell/index.tsx
+++ b/src/components/flow/FlowCell/index.tsx
@@ -1,4 +1,4 @@
-import React, { FC } from 'react';
+import React, { FC, useState } from 'react';
 import styles from './styles.module.scss';
 import { NavLink } from 'react-router-dom';
 import { CellShade } from '~/components/flow/CellShade';
@@ -8,6 +8,10 @@ import { FlowCellText } from '~/components/flow/FlowCellText';
 import classNames from 'classnames';
 import { FlowCellMenu } from '~/components/flow/FlowCellMenu';
 import { useFlowCellControls } from '~/utils/hooks/flow/useFlowCellControls';
+import { Icon } from '~/components/input/Icon';
+import { useFocusEvent } from '~/utils/hooks/useFocusEvent';
+import { useClickOutsideFocus } from '~/utils/hooks/useClickOutsideFocus';
+import { MenuDots } from '~/components/common/MenuDots';
 
 interface Props {
   id: INode['id'];
@@ -29,10 +33,11 @@ const FlowCell: FC<Props> = ({
   flow,
   text,
   title,
-  canEdit,
+  canEdit = false,
   onChangeCellView,
 }) => {
-  const withText = ((!!flow.display && flow.display !== 'single') || !image) && !!text;
+  const withText =
+    ((!!flow.display && flow.display !== 'single') || !image) && flow.show_description && !!text;
   const {
     hasDescription,
     setViewHorizontal,
@@ -41,12 +46,22 @@ const FlowCell: FC<Props> = ({
     setViewSingle,
     toggleViewDescription,
   } = useFlowCellControls(id, text, flow, onChangeCellView);
+  const { isActive: isMenuActive, activate, ref, deactivate } = useClickOutsideFocus();
 
   return (
-    <div className={classNames(styles.cell, styles[flow.display || 'single'])}>
-      {canEdit && (
+    <div className={classNames(styles.cell, styles[flow.display || 'single'])} ref={ref as any}>
+      {canEdit && !isMenuActive && (
         <div className={styles.menu}>
+          <MenuDots onClick={activate} />
+        </div>
+      )}
+
+      {canEdit && isMenuActive && (
+        <div className={styles.display_modal}>
           <FlowCellMenu
+            onClose={deactivate}
+            currentView={flow.display}
+            descriptionEnabled={flow.show_description}
             hasDescription={hasDescription}
             setViewHorizontal={setViewHorizontal}
             setViewQuadro={setViewQuadro}
@@ -56,6 +71,7 @@ const FlowCell: FC<Props> = ({
           />
         </div>
       )}
+
       <NavLink className={styles.link} to={to}>
         {withText && (
           <FlowCellText className={styles.text} heading={<h4 className={styles.title}>{title}</h4>}>
diff --git a/src/components/flow/FlowCell/styles.module.scss b/src/components/flow/FlowCell/styles.module.scss
index 370d061d..57b14f20 100644
--- a/src/components/flow/FlowCell/styles.module.scss
+++ b/src/components/flow/FlowCell/styles.module.scss
@@ -107,3 +107,51 @@
     flex-direction: column-reverse;
   }
 }
+
+.display_modal {
+  @include appear;
+
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 11;
+}
+
+.button {
+  width: 48px;
+  height: 48px;
+  display: flex;
+  align-items: flex-start;
+  justify-content: flex-end;
+  fill: white;
+  padding: 7px;
+  box-sizing: border-box;
+  cursor: pointer;
+}
+
+.dots {
+  @include blur($content_bg, 5px, 0.7);
+
+  padding: 5px 0 0 0;
+  background: $content_bg;
+  border-radius: $radius;
+  width: 18px;
+  height: 30px;
+  position: relative;
+
+  svg {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+
+    opacity: 0.5;
+    transition: opacity 0.25s;
+
+    :hover > & {
+      opacity: 1;
+    }
+  }
+}
diff --git a/src/components/flow/FlowCellMenu/index.tsx b/src/components/flow/FlowCellMenu/index.tsx
index dd6a7874..78cc8b32 100644
--- a/src/components/flow/FlowCellMenu/index.tsx
+++ b/src/components/flow/FlowCellMenu/index.tsx
@@ -1,12 +1,15 @@
 import React, { FC } from 'react';
 import styles from './styles.module.scss';
 import { Icon } from '~/components/input/Icon';
-import { Manager, Popper, Reference } from 'react-popper';
-import { useFocusEvent } from '~/utils/hooks/useFocusEvent';
 import classNames from 'classnames';
-import { usePopperModifiers } from '~/utils/hooks/usePopperModifiers';
+import { Toggle } from '~/components/input/Toggle';
+import { Group } from '~/components/containers/Group';
+import { FlowDisplayVariant } from '~/redux/types';
 
 interface Props {
+  onClose: () => void;
+  currentView: FlowDisplayVariant;
+  descriptionEnabled: boolean;
   hasDescription: boolean;
   toggleViewDescription: () => void;
   setViewSingle: () => void;
@@ -16,51 +19,51 @@ interface Props {
 }
 
 const FlowCellMenu: FC<Props> = ({
+  onClose,
   hasDescription,
   toggleViewDescription,
+  descriptionEnabled,
   setViewSingle,
   setViewHorizontal,
   setViewVertical,
   setViewQuadro,
 }) => {
-  const { onFocus, onBlur, focused } = useFocusEvent();
-  const modifiers = usePopperModifiers(0, 10);
-
   return (
-    <Manager>
-      <button className={styles.button} onFocus={onFocus} onBlur={onBlur}>
-        <Reference>
-          {({ ref }) => (
-            <div className={styles.dots} ref={ref}>
-              <Icon icon="menu" size={24} />
-            </div>
-          )}
-        </Reference>
-      </button>
+    <div className={classNames(styles.dropdown)}>
+      {onClose && (
+        <button className={styles.close} onClick={onClose} type="button">
+          <Icon icon="close" size={24} />
+        </button>
+      )}
 
-      <Popper placement="bottom" strategy="fixed" modifiers={modifiers}>
-        {({ ref, style }) => (
-          <div
-            ref={ref}
-            style={style}
-            className={classNames(styles.dropdown, { [styles.active]: focused })}
-          >
-            <div className={styles.menu}>
-              {hasDescription && (
-                <>
-                  <Icon icon="text" onMouseDown={toggleViewDescription} size={32} />
-                  <div className={styles.sep} />
-                </>
-              )}
-              <Icon icon="cell-single" onMouseDown={setViewSingle} size={32} />
-              <Icon icon="cell-double-h" onMouseDown={setViewHorizontal} size={32} />
-              <Icon icon="cell-double-v" onMouseDown={setViewVertical} size={32} />
-              <Icon icon="cell-quadro" onMouseDown={setViewQuadro} size={32} />
-            </div>
-          </div>
+      <div className={styles.menu}>
+        <div className={styles.display}>
+          <Icon icon="cell-single" onMouseDown={setViewSingle} size={48} />
+          <Icon
+            icon={descriptionEnabled ? 'cell-double-h-text' : 'cell-double-h'}
+            onMouseDown={setViewHorizontal}
+            size={48}
+          />
+          <Icon
+            icon={descriptionEnabled ? 'cell-double-v-text' : 'cell-double-v'}
+            onMouseDown={setViewVertical}
+            size={48}
+          />
+          <Icon
+            icon={descriptionEnabled ? 'cell-quadro-text' : 'cell-quadro'}
+            onMouseDown={setViewQuadro}
+            size={48}
+          />
+        </div>
+
+        {hasDescription && (
+          <Group className={styles.description} horizontal onClick={toggleViewDescription}>
+            <Toggle color="white" value={descriptionEnabled} />
+            <span>Текст</span>
+          </Group>
         )}
-      </Popper>
-    </Manager>
+      </div>
+    </div>
   );
 };
 
diff --git a/src/components/flow/FlowCellMenu/styles.module.scss b/src/components/flow/FlowCellMenu/styles.module.scss
index ffd5da55..b59f7318 100644
--- a/src/components/flow/FlowCellMenu/styles.module.scss
+++ b/src/components/flow/FlowCellMenu/styles.module.scss
@@ -1,53 +1,16 @@
 @import "~/styles/variables";
 
-.button {
-  width: 48px;
-  height: 48px;
-  display: flex;
-  align-items: flex-start;
-  justify-content: flex-end;
-  fill: white;
-  padding: 5px;
-  box-sizing: border-box;
-  cursor: pointer;
-}
-
-.dots {
-  @include blur($content_bg, 5px, 0.7);
-
-  padding: 5px 0 0 0;
-  background: $content_bg;
-  border-radius: $radius;
-  width: 20px;
-  height: 32px;
-  position: relative;
-
-  svg {
-    position: absolute;
-    top: 50%;
-    left: 50%;
-    transform: translate(-50%, -50%);
-
-    opacity: 0.5;
-    transition: opacity 0.25s;
-
-    :hover > & {
-      opacity: 1;
-    }
-  }
-}
-
 .dropdown {
-  @include dropdown_shadow;
+  @include outer_shadow;
   @include blur($red, 15px, 0.3);
 
+  width: 100%;
+  height: 100%;
   border-radius: $radius;
   padding: $gap;
-  visibility: hidden;
-
-  &.active {
-    visibility: visible;
-  }
+  display: flex;
+  align-items: center;
+  justify-content: center;
 }
 
 .menu {
@@ -67,3 +30,35 @@
   @include outer_shadow;
   height: 1px;
 }
+
+.display {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  grid-row-gap: $gap;
+  grid-column-gap: $gap;
+}
+
+.description {
+  margin-top: $gap;
+  font: $font_12_semibold;
+  text-transform: uppercase;
+  display: flex;
+  flex-direction: row;
+  padding: 0 4px;
+
+  span {
+    flex: 1;
+    text-align: right;
+  }
+}
+
+.close {
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 32px;
+  height: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
diff --git a/src/components/flow/FlowGrid/index.tsx b/src/components/flow/FlowGrid/index.tsx
index 6af28ff2..4bcdf6ef 100644
--- a/src/components/flow/FlowGrid/index.tsx
+++ b/src/components/flow/FlowGrid/index.tsx
@@ -30,7 +30,7 @@ export const FlowGrid: FC<IProps> = ({ user, nodes, onChangeCellView }) => {
             to={URLS.NODE_URL(node.id)}
             image={getURLFromString(node.thumbnail, PRESETS.cover)}
             flow={node.flow}
-            text={node.flow.show_description ? node.description : ''}
+            text={node.description}
             title={node.title}
             canEdit={canEditNode(node, user)}
             onChangeCellView={onChangeCellView}
diff --git a/src/components/input/Toggle/index.tsx b/src/components/input/Toggle/index.tsx
index f36ee358..bb591012 100644
--- a/src/components/input/Toggle/index.tsx
+++ b/src/components/input/Toggle/index.tsx
@@ -1,16 +1,17 @@
 import React, { FC, useCallback } from 'react';
 import styles from './styles.module.scss';
 import classNames from 'classnames';
+import { ButtonProps, DivProps } from '~/utils/types';
 
-type ToggleColor = 'primary' | 'secondary' | 'lab' | 'danger';
+type ToggleColor = 'primary' | 'secondary' | 'lab' | 'danger' | 'white';
 
-interface IProps {
+type IProps = Omit<ButtonProps, 'value' | 'color'> & {
   value?: boolean;
   handler?: (val: boolean) => void;
   color?: ToggleColor;
-}
+};
 
-const Toggle: FC<IProps> = ({ value, handler, color = 'primary' }) => {
+const Toggle: FC<IProps> = ({ value, handler, color = 'primary', ...rest }) => {
   const onClick = useCallback(() => {
     if (!handler) {
       return;
@@ -21,8 +22,14 @@ const Toggle: FC<IProps> = ({ value, handler, color = 'primary' }) => {
 
   return (
     <button
+      {...rest}
       type="button"
-      className={classNames(styles.toggle, { [styles.active]: value }, styles[color])}
+      className={classNames(
+        styles.toggle,
+        { [styles.active]: value },
+        styles[color],
+        rest.className
+      )}
       onClick={onClick}
     />
   );
diff --git a/src/components/input/Toggle/styles.module.scss b/src/components/input/Toggle/styles.module.scss
index 00060eab..3f3edd42 100644
--- a/src/components/input/Toggle/styles.module.scss
+++ b/src/components/input/Toggle/styles.module.scss
@@ -12,6 +12,19 @@
   cursor: pointer;
   position: relative;
 
+  &.white {
+    box-shadow: inset white 0 0 0 2px;
+
+    &::after {
+      width: 14px;
+      height: 14px;
+      top: 5px;
+      left: 5px;
+      background: none;
+      box-shadow: inset white 0 0 0 2px;
+    }
+  }
+
   &::after {
     content: ' ';
     position: absolute;
@@ -26,7 +39,6 @@
   }
 
   &.active {
-
     &::after {
       transform: translate(24px, 0);
       background-color: white;
@@ -47,5 +59,9 @@
     &.danger {
       background-color: $red;
     }
+
+    &.monochrome {
+      background-color: white;
+    }
   }
 }
diff --git a/src/sprites/Sprites.tsx b/src/sprites/Sprites.tsx
index 530ad045..b7e34144 100644
--- a/src/sprites/Sprites.tsx
+++ b/src/sprites/Sprites.tsx
@@ -14,27 +14,92 @@ const Sprites: FC = () => (
       </pattern>
     </defs>
 
-    <g id="cell-single" stroke="none" transform="translate(2 2)">
-      <path d="M0,0 L9,0 L9,9 L0,9 L0,0 Z" fill="url(#pattern_stripes)" />
-      <path d="M11,0 L20,0 L20,9 L11,9 L11,0 Z M12,1 L12,8 L19,8 L19,1 L12,1 Z" />
-      <path d="M11,11 L20,11 L20,20 L11,20 L11,11 Z M12,12 L12,19 L19,19 L19,12 L12,12 Z" />
-      <path d="M0,11 L9,11 L9,20 L0,20 L0,11 Z M1,12 L1,19 L8,19 L8,12 L1,12 Z" />
+    <g id="cell-single" stroke="none">
+      <rect x="13.5" y="2.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" />
+      <rect x="13.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" />
+      <rect x="2.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" />
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M4 2C2.89543 2 2 2.89543 2 4V9C2 10.1046 2.89543 11 4 11H9C10.1046 11 11 10.1046 11 9V4C11 2.89543 10.1046 2 9 2H4ZM4 8H7C7.55228 8 8 8.44772 8 9C8 9.55228 7.55228 10 7 10H4C3.44772 10 3 9.55228 3 9C3 8.44772 3.44772 8 4 8Z"
+        fill="currentColor"
+        stroke="none"
+      />
     </g>
 
-    <g id="cell-double-h" stroke="none" transform="translate(2 2)">
-      <path d="M0,0 L19,0 L19,9 L0,9 L0,0 Z" fill="url(#pattern_stripes)" />
-      <path d="M11,11 L20,11 L20,20 L11,20 L11,11 Z M12,12 L12,19 L19,19 L19,12 L12,12 Z" />
-      <path d="M0,11 L9,11 L9,20 L0,20 L0,11 Z M1,12 L1,19 L8,19 L8,12 L1,12 Z" />
+    <g id="cell-double-h" stroke="none">
+      <rect x="13.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" />
+      <rect x="2.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" />
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M4 2C2.89543 2 2 2.89543 2 4V9C2 10.1046 2.89543 11 4 11H20C21.1046 11 22 10.1046 22 9V4C22 2.89543 21.1046 2 20 2H4ZM4 8C3.44772 8 3 8.44772 3 9C3 9.55228 3.44772 10 4 10H7C7.55228 10 8 9.55228 8 9C8 8.44772 7.55228 8 7 8H4Z"
+        fill="currentColor"
+        stroke="none"
+      />
     </g>
 
-    <g id="cell-double-v" stroke="none" transform="translate(2 2)">
-      <path d="M0,0 L9,0 L9,19 L0,19 L0,0 Z" fill="url(#pattern_stripes)" />
-      <path d="M11,0 L20,0 L20,9 L11,9 L11,0 Z M12,1 L12,8 L19,8 L19,1 L12,1 Z" />
-      <path d="M11,11 L20,11 L20,20 L11,20 L11,11 Z M12,12 L12,19 L19,19 L19,12 L12,12 Z" />
+    <g id="cell-double-h-text" stroke="none">
+      <rect x="13.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" />
+      <rect x="2.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" />
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M4 2C2.89543 2 2 2.89543 2 4V9C2 10.1046 2.89543 11 4 11H20C21.1046 11 22 10.1046 22 9V4C22 2.89543 21.1046 2 20 2H4ZM4 3C3.44772 3 3 3.44772 3 4V9C3 9.55228 3.44772 10 4 10H10C10.5523 10 11 9.55228 11 9V4C11 3.44772 10.5523 3 10 3H4Z"
+        fill="currentColor"
+        stroke="none"
+      />
+      <rect x="4" y="4" width="5" height="1" rx="0.5" fill="currentColor" stroke="none" />
+      <rect x="4" y="6" width="6" height="1" rx="0.5" fill="currentColor" stroke="none" />
+      <rect x="4" y="8" width="6" height="1" rx="0.5" fill="currentColor" stroke="none" />
     </g>
 
-    <g id="cell-quadro" stroke="none" transform="translate(2 2)">
-      <path d="M0,0 L19,0 L19,19 L0,19 L0,0 Z" fill="url(#pattern_stripes)" />
+    <g id="cell-double-v" stroke="none">
+      <rect x="13.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" />
+      <rect x="13.5" y="2.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" />
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M4 2C2.89543 2 2 2.89543 2 4V20C2 21.1046 2.89543 22 4 22H9C10.1046 22 11 21.1046 11 20V4C11 2.89543 10.1046 2 9 2H4ZM4 19C3.44772 19 3 19.4477 3 20C3 20.5523 3.44772 21 4 21H7C7.55228 21 8 20.5523 8 20C8 19.4477 7.55228 19 7 19H4Z"
+        fill="currentColor"
+        stroke="none"
+      />
+    </g>
+    <g id="cell-double-v-text" stroke="none">
+      <rect x="13.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" />
+      <rect x="13.5" y="2.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" />
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M4 2C2.89543 2 2 2.89543 2 4V20C2 21.1046 2.89543 22 4 22H9C10.1046 22 11 21.1046 11 20V4C11 2.89543 10.1046 2 9 2H4ZM4 14C3.44772 14 3 14.4477 3 15V20C3 20.5523 3.44772 21 4 21H9C9.55228 21 10 20.5523 10 20V15C10 14.4477 9.55228 14 9 14H4Z"
+        fill="currentColor"
+        stroke="none"
+      />
+      <rect x="4" y="15" width="3" height="1" rx="0.5" fill="currentColor" stroke="none" />
+      <rect x="4" y="17" width="5" height="1" rx="0.5" fill="currentColor" stroke="none" />
+      <rect x="4" y="19" width="5" height="1" rx="0.5" fill="currentColor" stroke="none" />
+    </g>
+
+    <g id="cell-quadro" stroke="none">
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M4 2C2.89543 2 2 2.89543 2 4V20C2 21.1046 2.89543 22 4 22H20C21.1046 22 22 21.1046 22 20V4C22 2.89543 21.1046 2 20 2H4ZM4 19C3.44772 19 3 19.4477 3 20C3 20.5523 3.44772 21 4 21H12C12.5523 21 13 20.5523 13 20C13 19.4477 12.5523 19 12 19H4Z"
+        fill="currentColor"
+        stroke="none"
+      />
+    </g>
+    <g id="cell-quadro-text" stroke="none">
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M4 2C2.89543 2 2 2.89543 2 4V20C2 21.1046 2.89543 22 4 22H20C21.1046 22 22 21.1046 22 20V4C22 2.89543 21.1046 2 20 2H4ZM4 14C3.44772 14 3 14.4477 3 15V20C3 20.5523 3.44772 21 4 21H10C10.5523 21 11 20.5523 11 20V15C11 14.4477 10.5523 14 10 14H4Z"
+        fill="currentColor"
+        stroke="none"
+      />
+      <rect x="4" y="15" width="4" height="1" rx="0.5" fill="currentColor" stroke="none" />
+      <rect x="4" y="17" width="6" height="1" rx="0.5" fill="currentColor" stroke="none" />
+      <rect x="4" y="19" width="6" height="1" rx="0.5" fill="currentColor" stroke="none" />
     </g>
 
     <g id="play">
diff --git a/src/styles/variables.scss b/src/styles/variables.scss
index 5dda5ddd..c511e7fb 100644
--- a/src/styles/variables.scss
+++ b/src/styles/variables.scss
@@ -312,3 +312,12 @@ $sidebar_border: transparentize(white, 0.95);
     grid-auto-rows: 50vw;
   }
 }
+
+@mixin appear {
+  @keyframes __appear {
+    from { opacity: 0; }
+    to { opacity: 1; }
+  }
+
+  animation: __appear 0.25s forwards;
+}
diff --git a/src/utils/hooks/flow/useFlowCellControls.ts b/src/utils/hooks/flow/useFlowCellControls.ts
index 68c78e2c..28124dfa 100644
--- a/src/utils/hooks/flow/useFlowCellControls.ts
+++ b/src/utils/hooks/flow/useFlowCellControls.ts
@@ -9,7 +9,7 @@ export const useFlowCellControls = (
 ) => {
   const onChange = useCallback(
     (value: Partial<FlowDisplay>) => onChangeCellView(id, { ...flow, ...value }),
-    []
+    [flow, onChangeCellView]
   );
 
   const hasDescription = !!description && description.length > 32;
@@ -17,23 +17,23 @@ export const useFlowCellControls = (
   const toggleViewDescription = useCallback(() => {
     const show_description = !(flow && flow.show_description);
     onChange({ show_description });
-  }, [id, flow, onChange]);
+  }, [flow, onChange]);
 
   const setViewSingle = useCallback(() => {
     onChange({ display: 'single' });
-  }, [id, flow, onChange]);
+  }, [onChange]);
 
   const setViewHorizontal = useCallback(() => {
     onChange({ display: 'horizontal' });
-  }, [id, flow, onChange]);
+  }, [onChange]);
 
   const setViewVertical = useCallback(() => {
     onChange({ display: 'vertical' });
-  }, [id, flow]);
+  }, [onChange]);
 
   const setViewQuadro = useCallback(() => {
     onChange({ display: 'quadro' });
-  }, [id, flow, onChange]);
+  }, [onChange]);
 
   return {
     hasDescription,
diff --git a/src/utils/hooks/useClickOutsideFocus.ts b/src/utils/hooks/useClickOutsideFocus.ts
new file mode 100644
index 00000000..6fcf0677
--- /dev/null
+++ b/src/utils/hooks/useClickOutsideFocus.ts
@@ -0,0 +1,30 @@
+/**
+ * Handles blur by detecting clicks outside refs.
+ */
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+export const useClickOutsideFocus = () => {
+  const ref = useRef<HTMLElement>();
+  const [isActive, setIsActive] = useState(false);
+
+  const activate = useCallback(() => setIsActive(true), [setIsActive]);
+  const deactivate = useCallback(() => setIsActive(false), [setIsActive]);
+
+  useEffect(() => {
+    if (!isActive || !ref.current) {
+      return;
+    }
+
+    const deactivator = (event: MouseEvent) => {
+      if (!ref.current?.contains(event.target as Node)) {
+        deactivate();
+      }
+    };
+
+    document.addEventListener('mouseup', deactivator);
+
+    return () => document.removeEventListener('mouseup', deactivator);
+  }, [isActive]);
+
+  return { ref, isActive, activate, deactivate };
+};
diff --git a/src/utils/types.ts b/src/utils/types.ts
index 4698ec0e..438d56d3 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -11,3 +11,8 @@ export type IMGProps = React.DetailedHTMLProps<
   React.ImgHTMLAttributes<HTMLImageElement>,
   HTMLImageElement
 >;
+
+export type ButtonProps = React.DetailedHTMLProps<
+  React.ButtonHTMLAttributes<HTMLButtonElement>,
+  HTMLButtonElement
+>;