Przeglądaj źródła

Add Zen mode for distraction free drawing (#1450)

Aakansha Doshi 5 lat temu
rodzic
commit
1866074c07

+ 1 - 0
src/appState.ts

@@ -46,6 +46,7 @@ export function getDefaultAppState(): AppState {
     collaborators: new Map(),
     shouldCacheIgnoreZoom: false,
     showShortcutsDialog: false,
+    zenModeEnabled: false,
   };
 }
 

+ 17 - 0
src/components/App.tsx

@@ -192,6 +192,7 @@ class App extends React.Component<any, AppState> {
   }
 
   public render() {
+    const { zenModeEnabled } = this.state;
     const canvasDOMWidth = window.innerWidth;
     const canvasDOMHeight = window.innerHeight;
 
@@ -217,6 +218,8 @@ class App extends React.Component<any, AppState> {
             });
           }}
           onLockToggle={this.toggleLock}
+          zenModeEnabled={zenModeEnabled}
+          toggleZenMode={this.toggleZenMode}
         />
         <main>
           <canvas
@@ -771,6 +774,12 @@ class App extends React.Component<any, AppState> {
     }));
   };
 
+  toggleZenMode = () => {
+    this.setState({
+      zenModeEnabled: !this.state.zenModeEnabled,
+    });
+  };
+
   private destroySocketClient = () => {
     this.setState({
       isCollaborating: false,
@@ -1072,6 +1081,14 @@ class App extends React.Component<any, AppState> {
       });
     }
 
+    if (
+      !event[KEYS.CTRL_OR_CMD] &&
+      event.altKey &&
+      event.keyCode === KEYS.Z_KEY_CODE
+    ) {
+      this.toggleZenMode();
+    }
+
     if (event.code === "KeyC" && event.altKey && event.shiftKey) {
       this.copyToClipboardAsPng();
       event.preventDefault();

+ 4 - 0
src/components/FixedSideContainer.css

@@ -15,6 +15,10 @@
   z-index: 2;
 }
 
+.FixedSideContainer_side_top.zen-mode {
+  right: 42px;
+}
+
 /* TODO: if these are used, make sure to implement RTL support
 .FixedSideContainer_side_left {
   left: var(--margin);

+ 5 - 1
src/components/FixedSideContainer.tsx

@@ -5,14 +5,18 @@ import React from "react";
 type FixedSideContainerProps = {
   children: React.ReactNode;
   side: "top" | "left" | "right";
+  className?: string;
 };
 
 export function FixedSideContainer({
   children,
   side,
+  className,
 }: FixedSideContainerProps) {
   return (
-    <div className={`FixedSideContainer FixedSideContainer_side_${side}`}>
+    <div
+      className={`FixedSideContainer FixedSideContainer_side_${side} ${className}`}
+    >
       {children}
     </div>
   );

+ 5 - 0
src/components/Island.css → src/components/Island.scss

@@ -6,4 +6,9 @@
   border-radius: var(--border-radius-m);
   padding: calc(var(--padding) * var(--space-factor));
   position: relative;
+  transition: box-shadow 0.5s ease-in-out;
+
+  &.zen-mode {
+    box-shadow: none;
+  }
 }

+ 2 - 2
src/components/Island.tsx

@@ -1,11 +1,11 @@
-import "./Island.css";
+import "./Island.scss";
 
 import React from "react";
 
 type IslandProps = {
   children: React.ReactNode;
   padding?: number;
-  className?: string;
+  className?: string | boolean;
   style?: object;
 };
 

+ 44 - 0
src/components/LayerUI.scss

@@ -66,4 +66,48 @@
       visibility: visible;
     }
   }
+
+  &__github-corner {
+    top: 0;
+    right: 0;
+    position: absolute;
+    width: 40px;
+  }
+
+  &__footer {
+    position: absolute;
+    bottom: 0px;
+    right: 0;
+    width: 190px;
+  }
+
+  .zen-mode-transition {
+    transition: transform 0.5s ease-in-out;
+    &.transition-left {
+      transform: translate(-999px, 0);
+    }
+    &.transition-right {
+      transform: translate(999px, 0px);
+    }
+  }
+
+  .disable-zen-mode {
+    height: 30px;
+    position: absolute;
+    bottom: 10px;
+    right: 15px;
+    font-size: 10px;
+    padding: 10px;
+    font-weight: 500;
+    opacity: 0;
+    visibility: hidden;
+    transition: visibility 0s linear 0s, opacity 0.5s;
+
+    &--visible {
+      opacity: 1;
+      visibility: visible;
+      transition: visibility 0s linear 300ms, opacity 0.5s;
+      transition-delay: 0.8s;
+    }
+  }
 }

+ 66 - 29
src/components/LayerUI.tsx

@@ -41,6 +41,8 @@ interface LayerUIProps {
   onUsernameChange: (username: string) => void;
   onRoomDestroy: () => void;
   onLockToggle: () => void;
+  zenModeEnabled: boolean;
+  toggleZenMode: () => void;
 }
 
 const LayerUI = ({
@@ -53,6 +55,8 @@ const LayerUI = ({
   onUsernameChange,
   onRoomDestroy,
   onLockToggle,
+  zenModeEnabled,
+  toggleZenMode,
 }: LayerUIProps) => {
   const isMobile = useIsMobile();
 
@@ -112,7 +116,10 @@ const LayerUI = ({
   };
 
   const renderCanvasActions = () => (
-    <Section heading="canvasActions">
+    <Section
+      heading="canvasActions"
+      className={`zen-mode-transition ${zenModeEnabled && "transition-left"}`}
+    >
       {/* the zIndex ensures this menu has higher stacking order,
          see https://github.com/excalidraw/excalidraw/pull/1445 */}
       <Island padding={4} style={{ zIndex: 1 }}>
@@ -138,7 +145,10 @@ const LayerUI = ({
   );
 
   const renderSelectedShapeActions = () => (
-    <Section heading="selectedShapeActions">
+    <Section
+      heading="selectedShapeActions"
+      className={`zen-mode-transition ${zenModeEnabled && "transition-left"}`}
+    >
       <Island className={CLASSES.SHAPE_ACTIONS_MENU} padding={4}>
         <SelectedShapeActions
           appState={appState}
@@ -167,7 +177,7 @@ const LayerUI = ({
             {(heading) => (
               <Stack.Col gap={4} align="start">
                 <Stack.Row gap={1}>
-                  <Island padding={1}>
+                  <Island padding={1} className={zenModeEnabled && "zen-mode"}>
                     {heading}
                     <Stack.Row gap={1}>
                       <ShapesSwitcher
@@ -177,6 +187,7 @@ const LayerUI = ({
                     </Stack.Row>
                   </Island>
                   <LockIcon
+                    zenModeEnabled={zenModeEnabled}
                     checked={appState.elementLocked}
                     onChange={onLockToggle}
                     title={t("toolBar.lock")}
@@ -187,34 +198,54 @@ const LayerUI = ({
           </Section>
           <div />
         </div>
-        <div className="App-menu App-menu_bottom">
-          <Stack.Col gap={2}>
-            <Section heading="canvasActions">
-              <Island padding={1}>
-                <ZoomActions
-                  renderAction={actionManager.renderAction}
-                  zoom={appState.zoom}
-                />
-              </Island>
-              {renderEncryptedIcon()}
-            </Section>
-          </Stack.Col>
-        </div>
+        {
+          <div
+            className={`App-menu App-menu_bottom zen-mode-transition ${
+              zenModeEnabled && "transition-left"
+            }`}
+          >
+            <Stack.Col gap={2}>
+              <Section heading="canvasActions">
+                <Island padding={1}>
+                  <ZoomActions
+                    renderAction={actionManager.renderAction}
+                    zoom={appState.zoom}
+                  />
+                </Island>
+                {renderEncryptedIcon()}
+              </Section>
+            </Stack.Col>
+          </div>
+        }
       </FixedSideContainer>
     );
   };
 
   const renderFooter = () => (
-    <footer role="contentinfo">
-      <LanguageList
-        onChange={(lng) => {
-          setLanguage(lng);
-          setAppState({});
-        }}
-        languages={languages}
-        floating
-      />
-      {actionManager.renderAction("toggleShortcuts")}
+    <footer role="contentinfo" className="layer-ui__wrapper__footer">
+      <div
+        className={`zen-mode-transition ${
+          zenModeEnabled && "transition-right"
+        }`}
+      >
+        <LanguageList
+          onChange={(lng) => {
+            setLanguage(lng);
+            setAppState({});
+          }}
+          languages={languages}
+          floating
+        />
+        {actionManager.renderAction("toggleShortcuts")}
+      </div>
+      <button
+        className={`disable-zen-mode ${
+          zenModeEnabled && "disable-zen-mode--visible"
+        }`}
+        onClick={toggleZenMode}
+      >
+        {t("buttons.exitZenMode")}
+      </button>
       {appState.scrolledOutside && (
         <button
           className="scroll-back-to-content"
@@ -255,9 +286,15 @@ const LayerUI = ({
         />
       )}
       {renderFixedSideContainer()}
-      <aside>
-        <GitHubCorner />
-      </aside>
+      {
+        <aside
+          className={`layer-ui__wrapper__github-corner zen-mode-transition ${
+            zenModeEnabled && "transition-right"
+          }`}
+        >
+          <GitHubCorner />
+        </aside>
+      }
       {renderFooter()}
     </div>
   );

+ 4 - 1
src/components/LockIcon.tsx

@@ -11,6 +11,7 @@ type LockIconProps = {
   checked: boolean;
   onChange?(): void;
   size?: LockIconSize;
+  zenModeEnabled?: boolean;
 };
 
 const DEFAULT_SIZE: LockIconSize = "m";
@@ -44,7 +45,9 @@ export function LockIcon(props: LockIconProps) {
 
   return (
     <label
-      className={`ToolIcon ToolIcon__lock ToolIcon_type_floating ${sizeCn}`}
+      className={`ToolIcon ToolIcon__lock ToolIcon_type_floating ${sizeCn} zen-mode-visibility ${
+        props.zenModeEnabled && "hidden"
+      }`}
       title={`${props.title} — Q`}
     >
       <input

+ 4 - 0
src/components/ShortcutsDialog.tsx

@@ -236,6 +236,10 @@ export const ShortcutsDialog = ({ onClose }: { onClose?: () => void }) => {
                 label={t("buttons.toggleFullScreen")}
                 shortcuts={["F"]}
               />
+              <Shortcut
+                label={t("buttons.toggleZenMode")}
+                shortcuts={["Alt+Z"]}
+              />
             </ShortcutIsland>
           </Column>
           <Column>

+ 5 - 5
src/components/ToolButton.tsx

@@ -49,16 +49,16 @@ export const ToolButton = React.forwardRef(function (
       <button
         className={`ToolIcon_type_button ToolIcon ${sizeCn}${
           props.selected ? " ToolIcon--selected" : ""
-        } ${props.className || ""}`}
+        } ${props.className || ""} ${
+          props.visible
+            ? "ToolIcon_type_button--hide"
+            : "ToolIcon_type_button--show"
+        }`}
         title={props.title}
         aria-label={props["aria-label"]}
         type="button"
         onClick={props.onClick}
         ref={innerRef}
-        style={{
-          visibility:
-            props.visible || props.visible == null ? "visible" : "hidden",
-        }}
       >
         <div className="ToolIcon__icon" aria-hidden="true">
           {props.icon || props.label}

+ 19 - 0
src/components/ToolIcon.scss

@@ -71,6 +71,13 @@
       background-color: var(--button-gray-3);
     }
   }
+
+  &--show {
+    visibility: visible;
+  }
+  &--hide {
+    visibility: hidden;
+  }
 }
 
 .ToolIcon_type_radio,
@@ -160,3 +167,15 @@
     right: 2px;
   }
 }
+
+.zen-mode-visibility {
+  visibility: visible;
+  opacity: 1;
+  transition: visibility 0s linear 0s, opacity 0.5s;
+
+  &.hidden {
+    visibility: hidden;
+    opacity: 0;
+    transition: visibility 0s linear 300ms, opacity 0.5s;
+  }
+}

+ 1 - 0
src/keys.ts

@@ -15,6 +15,7 @@ export const KEYS = {
   QUESTION_MARK: "?",
   F_KEY_CODE: 70,
   ALT_KEY_CODE: 18,
+  Z_KEY_CODE: 90,
 } as const;
 
 export type Key = keyof typeof KEYS;

+ 3 - 1
src/locales/en.json

@@ -79,7 +79,9 @@
     "redo": "Redo",
     "roomDialog": "Start live collaboration",
     "createNewRoom": "Create new room",
-    "toggleFullScreen": "Toggle full screen"
+    "toggleFullScreen": "Toggle full screen",
+    "toggleZenMode": "Toggle zen mode",
+    "exitZenMode": "Exit zen mode"
   },
   "alerts": {
     "clearReset": "This will clear the whole canvas. Are you sure?",

+ 41 - 0
src/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -42,6 +42,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -244,6 +245,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -364,6 +366,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -645,6 +648,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -808,6 +812,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -1012,6 +1017,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -1275,6 +1281,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -1654,6 +1661,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -2285,6 +2293,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -2405,6 +2414,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -2525,6 +2535,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -2645,6 +2656,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -2787,6 +2799,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -2929,6 +2942,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -3071,6 +3085,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -3191,6 +3206,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -3311,6 +3327,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -3453,6 +3470,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -3573,6 +3591,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -3646,6 +3665,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -4549,6 +4569,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -4982,6 +5003,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -5320,6 +5342,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -5567,6 +5590,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -5743,6 +5767,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -6596,6 +6621,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -7338,6 +7364,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -7973,6 +8000,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -8506,6 +8534,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -8988,6 +9017,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -9373,6 +9403,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -9665,6 +9696,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -9884,6 +9916,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -10794,6 +10827,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -11591,6 +11625,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -12279,6 +12314,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -12858,6 +12894,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -13242,6 +13279,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -13299,6 +13337,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -13356,6 +13395,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;
@@ -13780,6 +13820,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
   "zoom": 1,
 }
 `;

+ 1 - 0
src/types.ts

@@ -64,6 +64,7 @@ export type AppState = {
   >;
   shouldCacheIgnoreZoom: boolean;
   showShortcutsDialog: boolean;
+  zenModeEnabled: boolean;
 };
 
 export type PointerCoords = Readonly<{