feat: add pane resizing support

(fixes streetwriters/notesnook#205)
This commit is contained in:
thecodrr
2022-04-14 15:09:52 +05:00
parent f1023bbc34
commit 5da38c3dda
10 changed files with 281 additions and 342 deletions

View File

@@ -16,6 +16,7 @@
"@streetwriters/tinymce-plugins": "^1.5.18",
"@tinymce/tinymce-react": "^3.13.0",
"@types/rebass": "^4.0.10",
"allotment": "^1.12.1",
"async-mutex": "^0.3.2",
"axios": "^0.21.4",
"clipboard-polyfill": "^3.0.3",

View File

@@ -17,7 +17,7 @@ export default function MobileAppEffects({ sliderId, overlayId, setShow }) {
onSliding: (e, { lastSlide, position, lastPosition }) => {
if (!isMobile) return;
const offset = 70;
const width = 180;
const width = 300;
const percent = offset - (position / width) * offset;
const overlay = document.getElementById("overlay");
@@ -32,6 +32,7 @@ export default function MobileAppEffects({ sliderId, overlayId, setShow }) {
onChange: (e, { slide, lastSlide }) => {
if (!lastSlide || !isMobile) return;
toggleSideMenu(slide?.index === 0 ? true : false);
console.log("Setting editor", slide?.index === 2 ? true : false);
setIsEditorOpen(slide?.index === 2 ? true : false);
},
});
@@ -43,6 +44,7 @@ export default function MobileAppEffects({ sliderId, overlayId, setShow }) {
useEffect(() => {
if (!isMobile) return;
console.log(isEditorOpen);
slideToIndex(isEditorOpen ? 2 : 1);
}, [isMobile, slideToIndex, isEditorOpen]);

View File

@@ -119,3 +119,28 @@ textarea,
background-color: var(--dimPrimary);
color: var(--text);
}
.middle-pane {
overflow: hidden;
display: flex;
}
.editor-pane {
overflow: hidden;
display: flex;
flex-direction: column;
}
.nav-pane {
display: flex;
flex-direction: column;
flex: 1;
}
.pane::before {
width: 1px !important;
}
:root {
--focus-border: var(--primary);
--separator-border: var(--border);
--sash-size: 10px;
--sash-hover-size: 4px;
}

View File

@@ -1,15 +1,17 @@
import React, { useState, Suspense } from "react";
import React, { useState, Suspense, useMemo, useRef, useEffect } from "react";
import { Box, Flex } from "rebass";
import ThemeProvider from "./components/theme-provider";
import { AnimatedFlex } from "./components/animated";
import NavigationMenuPlaceholder from "./components/navigationmenu/index.lite";
import StatusBarPlaceholder from "./components/statusbar/index.lite";
import useMobile from "./utils/use-mobile";
import useTablet from "./utils/use-tablet";
import { LazyMotion, domAnimation } from "framer-motion";
import useDatabase from "./hooks/use-database";
import Loader from "./components/loader";
import { Allotment } from "allotment";
import "allotment/dist/style.css";
import Config from "./utils/config";
import EditorLoading from "./components/editor/loading";
import NavigationMenuPlaceholder from "./components/navigationmenu/index.lite";
const GlobalMenuWrapper = React.lazy(() =>
import("./components/global-menu-wrapper")
@@ -22,9 +24,8 @@ const NavigationMenu = React.lazy(() => import("./components/navigation-menu"));
const StatusBar = React.lazy(() => import("./components/status-bar"));
function App() {
const [show, setShow] = useState(true);
const isMobile = useMobile();
const isTablet = useTablet();
const [show, setShow] = useState(true);
const [isAppLoaded] = useDatabase();
return (
@@ -52,116 +53,15 @@ function App() {
height="100%"
sx={{ overflow: "hidden" }}
>
<Flex
id="slider"
variant="rowFill"
overflowX={["auto", "hidden"]}
sx={{
overflowY: "hidden",
scrollSnapType: "x mandatory",
scrollBehavior: "smooth",
WebkitOverflowScrolling: "touch",
scrollSnapStop: "always",
overscrollBehavior: "contain",
}}
>
<Flex
flexShrink={0}
sx={{
scrollSnapAlign: "start",
scrollSnapStop: "always",
}}
>
<SuspenseLoader
condition={isAppLoaded}
component={NavigationMenu}
props={{
toggleNavigationContainer: (state) => {
if (!isMobile) setShow(state || !show);
},
}}
fallback={<NavigationMenuPlaceholder />}
/>
</Flex>
<AnimatedFlex
className="listMenu"
variant="columnFill"
initial={{
width: isMobile ? "100vw" : isTablet ? "40%" : "25%",
opacity: 1,
x: 0,
}}
animate={{
width: show
? isMobile
? "100vw"
: isTablet
? "40%"
: "25%"
: "0%",
x: show ? 0 : isTablet ? "-40%" : "-25%",
opacity: show ? 1 : 0,
}}
transition={{ duration: 0.2, ease: "easeOut" }}
sx={{
borderRight: "1px solid",
borderColor: "border",
borderRightWidth: show ? 1 : 0,
position: "relative",
scrollSnapAlign: "start",
scrollSnapStop: "always",
}}
flexShrink={0}
>
<SuspenseLoader
condition={isAppLoaded}
component={CachedRouter}
fallback={
<Loader
title="Did you know?"
text="All your notes are encrypted on your device."
/>
}
/>
{isMobile && (
<Box
id="overlay"
sx={{
position: "absolute",
width: "100%",
height: "100%",
top: 0,
left: 0,
zIndex: 999,
opacity: 0,
visibility: "visible",
pointerEvents: "none",
}}
bg="black"
/>
)}
</AnimatedFlex>
<Flex
width={["100vw", "100%"]}
flexShrink={[0, 1]}
sx={{
scrollSnapAlign: "start",
scrollSnapStop: "always",
}}
flexDirection="column"
>
<SuspenseLoader
fallback={<EditorLoading />}
component={HashRouter}
condition={isAppLoaded}
/>
</Flex>
</Flex>
<SuspenseLoader
fallback={<StatusBarPlaceholder />}
component={StatusBar}
condition={isAppLoaded}
/>
{isMobile ? (
<MobileAppContents isAppLoaded={isAppLoaded} />
) : (
<DesktopAppContents
isAppLoaded={isAppLoaded}
setShow={setShow}
show={show}
/>
)}
</Flex>
</ThemeProvider>
</LazyMotion>
@@ -179,3 +79,194 @@ function SuspenseLoader({ condition, props, component: Component, fallback }) {
</Suspense>
);
}
function DesktopAppContents({ isAppLoaded, show, setShow }) {
const isTablet = useTablet();
const defaultSizes = useMemo(
() => [isTablet ? 60 : 180, isTablet ? 240 : 380],
[isTablet]
);
const paneSizes = useMemo(
() => Config.get("paneSizes", defaultSizes),
[defaultSizes]
);
const [isNarrow, setIsNarrow] = useState(isTablet);
const panesRef = useRef();
useEffect(() => {
panesRef.current.reset();
}, [isTablet]);
return (
<>
<Flex
variant="rowFill"
sx={{
overflow: "hidden",
}}
>
<Allotment
ref={panesRef}
proportionalLayout
onChange={(sizes) => {
Config.set("paneSizes", sizes);
setIsNarrow(sizes[0] <= 132);
}}
>
<Allotment.Pane
className="pane nav-pane"
minSize={50}
preferredSize={paneSizes[0]}
>
<Flex flex={1}>
<SuspenseLoader
condition={isAppLoaded}
component={NavigationMenu}
props={{
toggleNavigationContainer: (state) => {
setShow(state || !show);
},
isTablet: isTablet || isNarrow,
}}
fallback={<NavigationMenuPlaceholder />}
/>
</Flex>
</Allotment.Pane>
<Allotment.Pane
className="pane middle-pane"
minSize={5}
preferredSize={paneSizes[1]}
visible={show}
>
<Flex className="listMenu" variant="columnFill">
<SuspenseLoader
condition={isAppLoaded}
component={CachedRouter}
fallback={
<Loader
title="Did you know?"
text="All your notes are encrypted on your device."
/>
}
/>
</Flex>
</Allotment.Pane>
<Allotment.Pane className="pane editor-pane">
<Flex
sx={{
overflow: "hidden",
flex: 1,
}}
flexDirection="column"
>
<SuspenseLoader
fallback={
<Loader
title="Fun fact"
text="Notesnook was released in January 2021 by a team of only 3 people."
/>
}
component={HashRouter}
condition={isAppLoaded}
/>
</Flex>
</Allotment.Pane>
</Allotment>
</Flex>
<SuspenseLoader
fallback={<StatusBarPlaceholder />}
component={StatusBar}
condition={isAppLoaded}
/>
</>
);
}
function MobileAppContents({ isAppLoaded }) {
return (
<Flex
id="slider"
variant="rowFill"
overflowX={"auto"}
sx={{
overflowY: "hidden",
scrollSnapType: "x mandatory",
scrollBehavior: "smooth",
WebkitOverflowScrolling: "touch",
scrollSnapStop: "always",
overscrollBehavior: "contain",
}}
>
<Flex
flexShrink={0}
sx={{
scrollSnapAlign: "start",
scrollSnapStop: "always",
width: [300, 60],
}}
>
<SuspenseLoader
condition={isAppLoaded}
component={NavigationMenu}
props={{
toggleNavigationContainer: () => {},
}}
fallback={<NavigationMenuPlaceholder />}
/>
</Flex>
<Flex
className="listMenu"
variant="columnFill"
width={"100vw"}
sx={{
position: "relative",
scrollSnapAlign: "start",
scrollSnapStop: "always",
}}
flexShrink={0}
>
<SuspenseLoader
condition={isAppLoaded}
component={CachedRouter}
fallback={
<Loader
title="Did you know?"
text="All your notes are encrypted on your device."
/>
}
/>
<Box
id="overlay"
sx={{
position: "absolute",
width: "100%",
height: "100%",
top: 0,
left: 0,
zIndex: 999,
opacity: 0,
visibility: "visible",
pointerEvents: "none",
}}
bg="black"
/>
</Flex>
<Flex
width={"100vw"}
flexShrink={0}
sx={{
scrollSnapAlign: "start",
scrollSnapStop: "always",
}}
flexDirection="column"
>
<SuspenseLoader
fallback={<EditorLoading />}
component={HashRouter}
condition={isAppLoaded}
/>
</Flex>
</Flex>
);
}

View File

@@ -227,6 +227,8 @@ function ListContainer(props) {
testId={`${props.type}-action-button`}
onClick={props.button.onClick}
sx={{
position: "absolute",
bottom: 0,
display: ["block", "block", "none"],
alignSelf: "end",
borderRadius: 100,

View File

@@ -70,7 +70,7 @@ const NAVIGATION_MENU_WIDTH = "10em";
const NAVIGATION_MENU_TABLET_WIDTH = "4em";
function NavigationMenu(props) {
const { toggleNavigationContainer } = props;
const { toggleNavigationContainer, isTablet } = props;
const [location, previousLocation, state] = useLocation();
const isFocusMode = useAppStore((store) => store.isFocusMode);
const colors = useAppStore((store) => store.colors);
@@ -104,7 +104,6 @@ function NavigationMenu(props) {
id="navigationmenu"
flexDirection="column"
justifyContent="space-between"
flex={1}
initial={{
opacity: 1,
}}
@@ -114,21 +113,10 @@ function NavigationMenu(props) {
}}
transition={{ duration: 0.3, ease: "easeOut" }}
sx={{
borderRight: "1px solid",
borderRightColor: "border",
minWidth: [
NAVIGATION_MENU_WIDTH,
isFocusMode ? 0 : NAVIGATION_MENU_TABLET_WIDTH,
isFocusMode ? 0 : NAVIGATION_MENU_WIDTH,
],
maxWidth: [
NAVIGATION_MENU_WIDTH,
isFocusMode ? 0 : NAVIGATION_MENU_TABLET_WIDTH,
isFocusMode ? 0 : NAVIGATION_MENU_WIDTH,
],
zIndex: 1,
height: "auto",
position: "relative",
flex: 1,
}}
bg={"bgSecondary"}
px={0}
@@ -137,6 +125,7 @@ function NavigationMenu(props) {
<Flex flexDirection="column">
{routes.map((item) => (
<NavigationItem
isTablet={isTablet}
key={item.path}
title={item.title}
icon={item.icon}
@@ -155,6 +144,7 @@ function NavigationMenu(props) {
))}
{colors.map((color) => (
<NavigationItem
isTablet={isTablet}
key={color.id}
title={db.colors.alias(color.id)}
icon={Circle}
@@ -186,6 +176,7 @@ function NavigationMenu(props) {
/>
{pins.map((pin) => (
<NavigationItem
isTablet={isTablet}
key={pin.id}
title={pin.type === "tag" ? db.tags.alias(pin.id) : pin.title}
menu={{
@@ -224,21 +215,9 @@ function NavigationMenu(props) {
</Flex>
</FlexScrollContainer>
<Flex flexDirection="column">
{/* {theme === "light" ? (
<NavigationItem
title="Dark mode"
icon={DarkMode}
onClick={toggleNightMode}
/>
) : (
<NavigationItem
title="Light mode"
icon={LightMode}
onClick={toggleNightMode}
/>
)} */}
{!isLoggedIn && (
<NavigationItem
isTablet={isTablet}
title="Login"
icon={Login}
onClick={() => hardNavigate("/login")}
@@ -246,6 +225,7 @@ function NavigationMenu(props) {
)}
<NavigationItem
isTablet={isTablet}
key={settings.path}
title={settings.title}
icon={settings.icon}
@@ -254,30 +234,32 @@ function NavigationMenu(props) {
}}
selected={location.startsWith(settings.path)}
>
<Button
variant={"icon"}
title="Toggle dark/light mode"
sx={{
position: "absolute",
right: "2px",
bg: "transparent",
borderRadius: "default",
":hover:not(disabled)": {
bg: "background",
},
}}
onClick={(e) => {
e.stopPropagation();
setFollowSystemTheme(false);
toggleNightMode();
}}
>
{theme === "dark" ? (
<DarkMode size={16} />
) : (
<LightMode size={16} />
)}
</Button>
{isTablet ? null : (
<Button
variant={"icon"}
title="Toggle dark/light mode"
sx={{
position: "absolute",
right: "2px",
bg: "bgSecondary",
borderRadius: "default",
":hover:not(disabled)": {
bg: "background",
},
}}
onClick={(e) => {
e.stopPropagation();
setFollowSystemTheme(false);
toggleNightMode();
}}
>
{theme === "dark" ? (
<DarkMode size={16} />
) : (
<LightMode size={16} />
)}
</Button>
)}
</NavigationItem>
</Flex>
</AnimatedFlex>

View File

@@ -1,60 +1,7 @@
import { Flex } from "rebass";
import { useStore as useThemeStore } from "../../stores/theme-store";
import { Button, Text } from "rebass";
import useTablet from "../../utils/use-tablet";
import {
Note,
Notebook,
StarOutline,
Monographs,
Tag,
Trash,
Settings,
DarkMode,
LightMode,
Sync,
Login,
} from "../icons";
import useLocation from "../../hooks/use-location";
const routes = [
{ title: "Notes", path: "/", icon: Note },
{
title: "Notebooks",
path: "/notebooks",
icon: Notebook,
},
{
title: "Favorites",
path: "/favorites",
icon: StarOutline,
},
{ title: "Tags", path: "/tags", icon: Tag },
{
title: "Monographs",
path: "/monographs",
icon: Monographs,
},
{ title: "Trash", path: "/trash", icon: Trash },
];
const bottomRoutes = [
{
title: "Settings",
path: "/settings",
icon: Settings,
},
];
const NAVIGATION_MENU_WIDTH = "10em";
const NAVIGATION_MENU_TABLET_WIDTH = "4em";
import Loader from "../loader";
function NavigationMenu() {
const isLoggedIn = false;
const theme = useThemeStore((store) => store.theme);
const toggleNightMode = useThemeStore((store) => store.toggleNightMode);
const [location] = useLocation();
return (
<Flex
id="navigationmenu"
@@ -64,127 +11,14 @@ function NavigationMenu() {
sx={{
borderRight: "1px solid",
borderRightColor: "border",
minWidth: [
NAVIGATION_MENU_WIDTH,
NAVIGATION_MENU_TABLET_WIDTH,
NAVIGATION_MENU_WIDTH,
],
maxWidth: [
NAVIGATION_MENU_WIDTH,
NAVIGATION_MENU_TABLET_WIDTH,
NAVIGATION_MENU_WIDTH,
],
zIndex: 1,
height: "auto",
position: "relative",
}}
bg={"bgSecondary"}
px={0}
>
<Flex
flexDirection="column"
sx={{
overflow: "scroll",
scrollbarWidth: "none",
"::-webkit-scrollbar": { width: 0, height: 0 },
msOverflowStyle: "none",
}}
>
{routes.map((item) => (
<NavigationItem
key={item.path}
title={item.title}
icon={item.icon}
selected={
item.path === "/"
? location === item.path
: location.startsWith(item.path)
}
/>
))}
</Flex>
<Flex flexDirection="column">
{theme === "light" ? (
<NavigationItem
title="Dark mode"
icon={DarkMode}
onClick={toggleNightMode}
/>
) : (
<NavigationItem
title="Light mode"
icon={LightMode}
onClick={toggleNightMode}
/>
)}
{isLoggedIn ? (
<>
<NavigationItem title="Sync" icon={Sync} />
</>
) : (
<NavigationItem title="Login" icon={Login} />
)}
{bottomRoutes.map((item) => (
<NavigationItem
key={item.path}
title={item.title}
icon={item.icon}
selected={location.startsWith(item.path)}
/>
))}
</Flex>
<Loader />
</Flex>
);
}
export default NavigationMenu;
function NavigationItem(props) {
const { icon: Icon, color, title, isLoading } = props;
const isTablet = useTablet();
return (
<Button
data-test-id={`navitem-${title.toLowerCase()}`}
variant="icon"
bg={props.selected ? "border" : "transparent"}
p={2}
mx={2}
mt={[1, 2, 1]}
sx={{
borderRadius: "default",
position: "relative",
":first-of-type": { mt: 2 },
":last-of-type": { mb: 2 },
}}
label={title}
title={title}
onClick={() => {
props.onClick();
}}
display="flex"
justifyContent={["flex-start", "center", "flex-start"]}
alignItems="center"
>
<Icon
size={isTablet ? 18 : 15}
color={color || (props.selected ? "primary" : "icon")}
rotate={isLoading}
/>
<Text
display={["block", "none", "block"]}
variant="body"
fontSize="subtitle"
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontWeight: props.selected ? "bold" : "normal",
}}
ml={1}
>
{title}
</Text>
</Button>
);
}

View File

@@ -2,7 +2,6 @@ import { Button, Text } from "rebass";
import { useStore as useAppStore } from "../../stores/app-store";
import { useMenuTrigger } from "../../hooks/use-menu";
import useMobile from "../../utils/use-mobile";
import useTablet from "../../utils/use-tablet";
import * as Icons from "../icons";
function NavigationItem(props) {
@@ -14,11 +13,11 @@ function NavigationItem(props) {
isShortcut,
isNew,
children,
isTablet,
} = props;
const toggleSideMenu = useAppStore((store) => store.toggleSideMenu);
const { openMenu } = useMenuTrigger();
const isMobile = useMobile();
const isTablet = useTablet();
return (
<Button
@@ -27,7 +26,7 @@ function NavigationItem(props) {
px={2}
py={"9px"}
mx={1}
mt={[1, 2, "3px"]}
mt={isTablet ? 1 : "3px"}
sx={{
borderRadius: "default",
position: "relative",
@@ -50,7 +49,7 @@ function NavigationItem(props) {
props.onClick();
}}
display="flex"
justifyContent={["flex-start", "center", "flex-start"]}
justifyContent={isTablet ? "center" : "flex-start"}
alignItems="center"
>
<Icon
@@ -74,7 +73,7 @@ function NavigationItem(props) {
)}
<Text
display={["block", "none", "block"]}
display={isTablet ? "none" : "block"}
variant="body"
fontSize="subtitle"
sx={{

View File

@@ -43,8 +43,11 @@ export default function useSlider(sliderId, { onSliding, onChange }) {
const slideToIndex = useCallback(
(index) => {
console.log(index, slides, ref.current, slides[index].node);
if (!ref.current || index >= slides.length) return;
slides[index].node.scrollIntoView({ behavior: "smooth" });
setTimeout(() => {
slides[index].node.scrollIntoView();
}, 100);
},
[ref, slides]
);

View File

@@ -220,7 +220,6 @@ class EditorStore extends BaseStore {
const session = this.get().session;
if (session.id) await db.fs.cancel(session.id);
appStore.setIsEditorOpen(false);
this.set((state) => {
state.session = {
...getDefaultSession(),
@@ -231,6 +230,7 @@ class EditorStore extends BaseStore {
this.toggleProperties(false);
if (shouldNavigate)
hashNavigate(`/notes/create`, { replace: true, addNonce: true });
appStore.setIsEditorOpen(false);
};
setTitle = (sessionId, title) => {