diff --git a/app/components/layout/BlurableContainer.js b/app/components/layout/BlurableContainer.js deleted file mode 100644 index bc77cdc572..0000000000 --- a/app/components/layout/BlurableContainer.js +++ /dev/null @@ -1,9 +0,0 @@ -import { modalVisible } from "connectors"; - -const BlurableContainer = ({ modalVisible, className, children }) => ( -
- {children} -
-); - -export default modalVisible(BlurableContainer); diff --git a/app/components/layout/BlurableContainer/BlurableContainer.jsx b/app/components/layout/BlurableContainer/BlurableContainer.jsx new file mode 100644 index 0000000000..b8b9ef1e24 --- /dev/null +++ b/app/components/layout/BlurableContainer/BlurableContainer.jsx @@ -0,0 +1,14 @@ +import { classNames } from "pi-ui"; +import { useModalVisible } from "./hooks"; +import styles from "./BlurableContainer.module.css"; + +const BlurableContainer = ({ className, children }) => { + const { modalVisible } = useModalVisible(); + return ( +
+ {children} +
+ ); +}; + +export default BlurableContainer; diff --git a/app/components/layout/BlurableContainer/BlurableContainer.module.css b/app/components/layout/BlurableContainer/BlurableContainer.module.css new file mode 100644 index 0000000000..fe2bf4cad8 --- /dev/null +++ b/app/components/layout/BlurableContainer/BlurableContainer.module.css @@ -0,0 +1,3 @@ +.blur { + filter: blur(5px); +} diff --git a/app/components/layout/BlurableContainer/hooks.js b/app/components/layout/BlurableContainer/hooks.js new file mode 100644 index 0000000000..6660a785f7 --- /dev/null +++ b/app/components/layout/BlurableContainer/hooks.js @@ -0,0 +1,10 @@ +import { useSelector } from "react-redux"; +import * as sel from "selectors"; + +export const useModalVisible = () => { + const modalVisible = useSelector(sel.modalVisible); + + return { + modalVisible + }; +}; diff --git a/app/components/layout/BlurableContainer/index.js b/app/components/layout/BlurableContainer/index.js new file mode 100644 index 0000000000..78f4f316f4 --- /dev/null +++ b/app/components/layout/BlurableContainer/index.js @@ -0,0 +1 @@ +export { default } from "./BlurableContainer"; diff --git a/app/components/layout/DescriptionHeader.js b/app/components/layout/DescriptionHeader.js deleted file mode 100644 index 67b193ae98..0000000000 --- a/app/components/layout/DescriptionHeader.js +++ /dev/null @@ -1,8 +0,0 @@ -const DescriptionHeader = ({ description, actionButton }) => ( -
-
{actionButton}
- {description} -
-); - -export default DescriptionHeader; diff --git a/app/components/layout/DescriptionHeader/DescriptionHeader.jsx b/app/components/layout/DescriptionHeader/DescriptionHeader.jsx new file mode 100644 index 0000000000..66d8cc8b20 --- /dev/null +++ b/app/components/layout/DescriptionHeader/DescriptionHeader.jsx @@ -0,0 +1,11 @@ +import { classNames } from "pi-ui"; +import styles from "./DescriptionHeader.module.css"; + +const DescriptionHeader = ({ description, actionButton, className }) => ( +
+
{actionButton}
+ {description} +
+); + +export default DescriptionHeader; diff --git a/app/components/layout/DescriptionHeader/DescriptionHeader.module.css b/app/components/layout/DescriptionHeader/DescriptionHeader.module.css new file mode 100644 index 0000000000..26d0a17146 --- /dev/null +++ b/app/components/layout/DescriptionHeader/DescriptionHeader.module.css @@ -0,0 +1,17 @@ +.header { + white-space: pre-wrap; + color: var(--header-desc-lighter-color); + font-size: 13px; + float: left; + width: 100%; + margin-left: 40px; +} + +.actionButton { + position: absolute; + right: 80px; +} + +.actionButton > .button { + min-width: 76px; +} diff --git a/app/components/layout/DescriptionHeader/index.js b/app/components/layout/DescriptionHeader/index.js new file mode 100644 index 0000000000..3e1b9d864f --- /dev/null +++ b/app/components/layout/DescriptionHeader/index.js @@ -0,0 +1 @@ +export { default } from "./DescriptionHeader"; diff --git a/app/components/layout/StandaloneHeader.js b/app/components/layout/StandaloneHeader.js deleted file mode 100644 index bc00c800c6..0000000000 --- a/app/components/layout/StandaloneHeader.js +++ /dev/null @@ -1,22 +0,0 @@ -import { TitleHeader } from "./TitleHeader"; -import DescriptionHeader from "./DescriptionHeader"; - -const StandaloneHeader = ({ - title, - description, - iconType, - actionButton -}) => { - return ( -
- - -
- ); -}; - -export default StandaloneHeader; diff --git a/app/components/layout/StandaloneHeader/StandaloneHeader.jsx b/app/components/layout/StandaloneHeader/StandaloneHeader.jsx new file mode 100644 index 0000000000..5f266b25b3 --- /dev/null +++ b/app/components/layout/StandaloneHeader/StandaloneHeader.jsx @@ -0,0 +1,21 @@ +import { TitleHeader } from "../TitleHeader"; +import DescriptionHeader from "../DescriptionHeader"; +import styles from "./StandaloneHeader.module.css"; + +const StandaloneHeader = ({ + title, + description, + iconType, + actionButton +}) => ( +
+ + +
+); + +export default StandaloneHeader; diff --git a/app/components/layout/StandaloneHeader/StandaloneHeader.module.css b/app/components/layout/StandaloneHeader/StandaloneHeader.module.css new file mode 100644 index 0000000000..c72e95d6ed --- /dev/null +++ b/app/components/layout/StandaloneHeader/StandaloneHeader.module.css @@ -0,0 +1,27 @@ +.header { + background-color: var(--background-back-color); + padding: 43px 60px 0px 63px; + position: absolute; + top: 0px; + right: 0px; + left: 0px; + height: 114px; +} + +@media screen and (max-width: 1179px) { + .header { + padding-left: 20px; + } +} + +@media screen and (max-width: 768px) { + .header { + height: initial; + display: flex; + flex-direction: column; + width: 100%; + position: relative; + padding-bottom: 48px; + padding-top: 30px; + } +} diff --git a/app/components/layout/StandaloneHeader/index.js b/app/components/layout/StandaloneHeader/index.js new file mode 100644 index 0000000000..24da5bd6b0 --- /dev/null +++ b/app/components/layout/StandaloneHeader/index.js @@ -0,0 +1 @@ +export { default } from "./StandaloneHeader"; diff --git a/app/components/layout/StandalonePage.js b/app/components/layout/StandalonePage.js deleted file mode 100644 index a52a2aadb2..0000000000 --- a/app/components/layout/StandalonePage.js +++ /dev/null @@ -1,17 +0,0 @@ -import StandalonePageBody from "./StandalonePageBody"; -import { classNames } from "pi-ui"; - -export default ({ header, children, className }) => { - const body = header ? ( - {children} - ) : ( - children - ); - - return ( -
- {header} - {body} -
- ); -}; diff --git a/app/components/layout/StandalonePage/StandalonePage.jsx b/app/components/layout/StandalonePage/StandalonePage.jsx new file mode 100644 index 0000000000..289d3e30a1 --- /dev/null +++ b/app/components/layout/StandalonePage/StandalonePage.jsx @@ -0,0 +1,20 @@ +import StandalonePageBody from "../StandalonePageBody"; +import { classNames } from "pi-ui"; +import styles from "./StandalonePage.module.css"; + +const StandalonePage = ({ header, children, className }) => { + const body = header ? ( + {children} + ) : ( + children + ); + + return ( +
+ {header} + {body} +
+ ); +}; + +export default StandalonePage; diff --git a/app/components/layout/StandalonePage/StandalonePage.module.css b/app/components/layout/StandalonePage/StandalonePage.module.css new file mode 100644 index 0000000000..b109e054a8 --- /dev/null +++ b/app/components/layout/StandalonePage/StandalonePage.module.css @@ -0,0 +1,7 @@ +.page { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} diff --git a/app/components/layout/StandalonePage/index.js b/app/components/layout/StandalonePage/index.js new file mode 100644 index 0000000000..71f1e241c9 --- /dev/null +++ b/app/components/layout/StandalonePage/index.js @@ -0,0 +1 @@ +export { default } from "./StandalonePage"; diff --git a/app/components/layout/StandalonePageBody.js b/app/components/layout/StandalonePageBody.js deleted file mode 100644 index a5706ff042..0000000000 --- a/app/components/layout/StandalonePageBody.js +++ /dev/null @@ -1,3 +0,0 @@ -export default ({ children }) => ( -
{children}
-); diff --git a/app/components/layout/StandalonePageBody/StandalonePageBody.jsx b/app/components/layout/StandalonePageBody/StandalonePageBody.jsx new file mode 100644 index 0000000000..8ec24cb075 --- /dev/null +++ b/app/components/layout/StandalonePageBody/StandalonePageBody.jsx @@ -0,0 +1,7 @@ +import styles from "./StandalonePageBody.module.css"; + +const StandalonePageBody = ({ children }) => ( +
{children}
+); + +export default StandalonePageBody; diff --git a/app/components/layout/StandalonePageBody/StandalonePageBody.module.css b/app/components/layout/StandalonePageBody/StandalonePageBody.module.css new file mode 100644 index 0000000000..c0912cf4e8 --- /dev/null +++ b/app/components/layout/StandalonePageBody/StandalonePageBody.module.css @@ -0,0 +1,23 @@ +.body { + overflow-x: hidden; + overflow-y: scroll; + position: absolute; + right: 0px; + top: 157px; + bottom: 0px; + left: 0px; + padding: 38px 60px 30px 63px; + background-color: var(--background-container); +} + +@media screen and (max-width: 1179px) { + .body { + padding-left: 20px; + } +} + +@media screen and (max-width: 768px) { + .body { + top: 113px; + } +} diff --git a/app/components/layout/StandalonePageBody/index.js b/app/components/layout/StandalonePageBody/index.js new file mode 100644 index 0000000000..d128e18f08 --- /dev/null +++ b/app/components/layout/StandalonePageBody/index.js @@ -0,0 +1 @@ +export { default } from "./StandalonePageBody"; diff --git a/app/components/layout/TabbedPage.js b/app/components/layout/TabbedPage.js deleted file mode 100644 index 213850f5f7..0000000000 --- a/app/components/layout/TabbedPage.js +++ /dev/null @@ -1,183 +0,0 @@ -import { classNames } from "pi-ui"; -import { Switch, Route, matchPath } from "react-router-dom"; -import { RoutedTabsHeader, RoutedTab } from "shared"; -import { TransitionMotion, spring } from "react-motion"; -import theme from "theme"; -import { - createElement as h, - cloneElement as k, - useEffect, - useState, - useReducer -} from "react"; -import { useSelector } from "react-redux"; -import * as sel from "selectors"; -import { usePrevious } from "hooks"; - -export const TabbedPageTab = ({ children }) => children; -TabbedPageTab.propTypes = { - path: PropTypes.string.isRequired, - link: PropTypes.node.isRequired -}; - -function getTabs(children) { - if (!Array.isArray(children)) children = [children]; - return children - .filter((c) => c.type === TabbedPageTab) - .map((c, i) => ({ index: i, tab: c })); -} - -function getMatchedTab(location, children) { - const tabs = getTabs(children); - return tabs.find( - (t) => !!matchPath(location.pathname, { path: t.tab.props.path }) - ); -} - -function getStyles(matchedTab) { - if (!matchedTab) { - return []; - } - - const element = React.isValidElement(matchedTab.tab.props.component) - ? k(matchedTab.tab.props.component, { - ...matchedTab.tab.props, - ...matchedTab.tab.props.component.props - }) - : // If the component props are needed, it is needed to make it a valid react element - // before send, otherwise they will be undfined. - h(matchedTab.tab.props.component, { ...matchedTab.tab.props }, null); - return [ - { - key: matchedTab.tab.props.path, - data: { matchedTab, element }, - style: { left: spring(0, theme("springs.tab")), opacity: 1 } - } - ]; -} - -function willLeave(dir) { - const pos = dir === "l2r" ? -1000 : +1000; - return { - left: spring(pos, { stiffness: 180, damping: 20 }), - opacity: spring(0) - }; -} - -function willEnter(dir) { - const pos = dir === "l2r" ? +1000 : -1000; - return { left: pos, opacity: 1 }; -} - -// returns the state.styles in a static container, without animations. -function staticStyles(styles) { - return ( - <> - {styles.map((s) => ( -
- {s.data.element} -
- ))} - - ); -} - -// returns the state.styles wrapped in a TransitionMotion, to show the animations -function animatedStyles(styles, dir) { - return ( - willLeave(dir)} - willEnter={() => willEnter(dir)}> - {(interpolatedStyles) => { - return ( - <> - {interpolatedStyles.map((s) => { - return ( -
990 ? "hidden" : "" - }} - key={s.key}> - {s.data.element} -
- ); - })} - - ); - }} -
- ); -} - -function TabbedPage({ children, header, className, onChange, caret }) { - const location = useSelector(sel.location); - const uiAnimations = useSelector(sel.uiAnimations); - const [matchedTab, setMatchedTab] = useReducer(() => - getMatchedTab(location, children) - ); - - const previous = usePrevious({ matchedTab, location }); - const [dir, setDir] = useState("l2r"); - useEffect(() => { - setMatchedTab(getMatchedTab(location, children)); - }, [children, location]); - useEffect(() => { - if (previous && previous.location.pathname === location.pathname) return; - if (typeof onChange === "function") onChange(); - const matchedTab = getMatchedTab(location, children); - if (!matchedTab) return; - // if (previous && previous.matchedTab) is false, it probably means it is the first time rendering - // therefore we use "l2r", as it is probably the first tab. - const dir = - previous && previous.matchedTab - ? previous.matchedTab.index > matchedTab.index - ? "r2l" - : "l2r" - : "l2r"; - setDir(dir); - setMatchedTab(matchedTab); - }, [location, previous, children, onChange]); - if (!Array.isArray(children)) children = [children]; - - const tabs = children.filter( - (c) => c.type === TabbedPageTab && !c.props.disabled - ); - const nonTabs = children.filter((c) => c.type !== TabbedPageTab); - - const tabHeaders = tabs.map((c) => RoutedTab(c.props.path, c.props.link)); - - const headers = tabs.map((c) => ( - - )); - - const styles = getStyles(matchedTab); - - const tabContents = uiAnimations - ? animatedStyles(styles, dir) - : staticStyles(styles); - - return ( -
-
- {header} - {headers} - -
- -
- {tabContents} - {nonTabs} -
-
- ); -} - -export default TabbedPage; diff --git a/app/components/layout/TabbedPage/TabbedPage.jsx b/app/components/layout/TabbedPage/TabbedPage.jsx new file mode 100644 index 0000000000..dad8ddd082 --- /dev/null +++ b/app/components/layout/TabbedPage/TabbedPage.jsx @@ -0,0 +1,146 @@ +import { classNames } from "pi-ui"; +import { Switch, Route } from "react-router-dom"; +import { TransitionMotion } from "react-motion"; +import { useEffect, useState, useReducer } from "react"; +import { useSelector } from "react-redux"; +import * as sel from "selectors"; +import { usePrevious } from "hooks"; +import { RoutedTabsHeader, RoutedTab } from "shared"; +import { getStyles, getMatchedTab, willEnter, willLeave } from "./helpers"; +import TabbedPageTab from "./TabbedPageTab"; +import styles from "./TabbedPage.module.css"; + +// returns the state.styles in a static container, without animations. +const staticStyles = (stylesObj, contentClassName) => ( + <> + {stylesObj.map(({ key, data: { element } }) => ( +
+ {element} +
+ ))} + +); + +// returns the state.styles wrapped in a TransitionMotion, to show the +// animations. +const animatedStyles = (stylesObj, dir, contentClassName) => ( + willLeave(dir)} + willEnter={() => willEnter(dir)}> + {(interpolatedStyles) => ( + <> + {interpolatedStyles.map( + ({ key, data: { element }, style: { left, opacity } }) => ( +
990 ? "hidden" : "" + }} + key={key}> + {element} +
+ ) + )} + + )} +
+); + +const TabbedPage = ({ + children, + className, + header, + headerClassName, + tabsClassName, + tabContentClassName, + onChange, + caret +}) => { + const location = useSelector(sel.location); + const uiAnimations = useSelector(sel.uiAnimations); + const [matchedTab, setMatchedTab] = useReducer(() => + getMatchedTab(location, children) + ); + const previous = usePrevious({ matchedTab, location }); + const [dir, setDir] = useState("l2r"); + + useEffect(() => { + setMatchedTab(getMatchedTab(location, children)); + }, [children, location]); + + useEffect(() => { + if (previous && previous.location.pathname === location.pathname) return; + if (typeof onChange === "function") onChange(); + const matchedTab = getMatchedTab(location, children); + if (!matchedTab) return; + // if (previous && previous.matchedTab) is false, it probably means it is + // the first time rendering therefore we use "l2r", as it is probably the + // first tab. + const dir = + previous && previous.matchedTab + ? previous.matchedTab.index > matchedTab.index + ? "r2l" + : "l2r" + : "l2r"; + setDir(dir); + setMatchedTab(matchedTab); + }, [location, previous, children, onChange]); + + if (!Array.isArray(children)) children = [children]; + + const tabs = children.filter( + ({ type, props: { disabled } }) => type === TabbedPageTab && !disabled + ); + + const nonTabs = children.filter(({ type }) => type !== TabbedPageTab); + + const tabHeaders = tabs.map( + ({ props: { path, link, className, activeClassName } }) => + RoutedTab(path, link, className, activeClassName) + ); + + const headers = tabs.map(({ props: { path, header } }) => ( + + )); + + const tabStyles = getStyles(matchedTab); + + const tabContents = uiAnimations + ? animatedStyles(tabStyles, dir, tabContentClassName) + : staticStyles(tabStyles, tabContentClassName); + + return ( +
+
+ {header} + {headers} + +
+ +
+ {tabContents} + {nonTabs} +
+
+ ); +}; + +export default TabbedPage; diff --git a/app/components/layout/TabbedPage/TabbedPage.module.css b/app/components/layout/TabbedPage/TabbedPage.module.css new file mode 100644 index 0000000000..99ef41bd65 --- /dev/null +++ b/app/components/layout/TabbedPage/TabbedPage.module.css @@ -0,0 +1,63 @@ +.tabbedPageBody { + overflow: hidden; + position: absolute; + right: 0; + top: 163px; + bottom: 0; + left: 0; + background-color: var(--background-container); +} + +.tabbedPageHeader { + background-color: var(--background-back-color); + padding: 43px 60px 0px 63px; + position: absolute; + top: 0px; + right: 0px; + left: 0px; + height: 114px; +} + +.tabContent { + position: absolute; + overflow-x: hidden; + right: 0; + top: 0; + bottom: 0; + left: 0; + padding: 38px 80px 30px 63px; + background-color: var(--background-container); +} + +.tabContent:not(.visible)::-webkit-scrollbar { + width: 5px; + background-color: transparent; +} + +.tabContent:not(.visible)::-webkit-scrollbar-thumb { + background-color: transparent; +} + +@media screen and (max-width: 1179px) { + .tabbedPageHeader { + padding-left: 20px; + } + .tabContent { + padding: 38px 20px 30px 20px; + } +} + +@media screen and (max-width: 768px) { + .tabbedPageBody { + top: 133px; + } + + .tabbedPageHeader { + height: 82px; + padding-top: 30px; + } + + .tabContent { + padding: 0 10px 80px 10px; + } +} diff --git a/app/components/layout/TabbedPage/TabbedPageTab.jsx b/app/components/layout/TabbedPage/TabbedPageTab.jsx new file mode 100644 index 0000000000..e1449030ea --- /dev/null +++ b/app/components/layout/TabbedPage/TabbedPageTab.jsx @@ -0,0 +1,8 @@ +const TabbedPageTab = ({ children }) => children; + +TabbedPageTab.propTypes = { + path: PropTypes.string.isRequired, + link: PropTypes.node.isRequired +}; + +export default TabbedPageTab; diff --git a/app/components/layout/TabbedPage/helpers.js b/app/components/layout/TabbedPage/helpers.js new file mode 100644 index 0000000000..f7f2d92e71 --- /dev/null +++ b/app/components/layout/TabbedPage/helpers.js @@ -0,0 +1,54 @@ +import { createElement as h, cloneElement as k } from "react"; +import { matchPath } from "react-router-dom"; +import { spring } from "react-motion"; +import theme from "theme"; +import TabbedPageTab from "./TabbedPageTab"; + +const getTabs = (children) => { + if (!Array.isArray(children)) children = [children]; + return children + .filter((c) => c.type === TabbedPageTab) + .map((c, i) => ({ index: i, tab: c })); +}; + +export const getMatchedTab = (location, children) => { + const tabs = getTabs(children); + return tabs.find( + (t) => !!matchPath(location.pathname, { path: t.tab.props.path }) + ); +}; + +export const getStyles = (matchedTab) => { + if (!matchedTab) { + return []; + } + + const element = React.isValidElement(matchedTab.tab.props.component) + ? k(matchedTab.tab.props.component, { + ...matchedTab.tab.props, + ...matchedTab.tab.props.component.props + }) + : // If the component props are needed, make a valid react element + // before send, otherwise they will be undfined. + h(matchedTab.tab.props.component, { ...matchedTab.tab.props }, null); + return [ + { + key: matchedTab.tab.props.path, + data: { matchedTab, element }, + style: { left: spring(0, theme("springs.tab")), opacity: 1 } + } + ]; +}; + +export const willLeave = (dir) => { + const pos = dir === "l2r" ? -1000 : +1000; + return { + left: spring(pos, { stiffness: 180, damping: 20 }), + opacity: spring(0) + }; +}; + +export const willEnter = (dir) => { + const pos = dir === "l2r" ? +1000 : -1000; + return { left: pos, opacity: 1 }; +}; diff --git a/app/components/layout/TabbedPage/index.js b/app/components/layout/TabbedPage/index.js new file mode 100644 index 0000000000..351ee13376 --- /dev/null +++ b/app/components/layout/TabbedPage/index.js @@ -0,0 +1,2 @@ +export { default as TabbedPageTab } from "./TabbedPageTab"; +export { default as TabbedPage } from "./TabbedPage"; diff --git a/app/components/layout/index.js b/app/components/layout/index.js index c596c106d8..7c86038553 100644 --- a/app/components/layout/index.js +++ b/app/components/layout/index.js @@ -1,4 +1,4 @@ -export { default as TabbedPage } from "./TabbedPage"; +export { TabbedPage } from "./TabbedPage"; export { TabbedPageTab } from "./TabbedPage"; export { default as StandaloneHeader } from "./StandaloneHeader"; export * from "./TitleHeader"; diff --git a/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.jsx b/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.jsx index c0cb62d106..16f744903b 100644 --- a/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.jsx +++ b/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.jsx @@ -1,22 +1,22 @@ -import { useState, useEffect, useCallback, useMemo, useRef } from "react"; +import { useMemo, useCallback } from "react"; import { NavLink as Link } from "react-router-dom"; import { spring, Motion } from "react-motion"; +import { classNames } from "pi-ui"; import { useRoutedTabsHeader } from "./hooks"; import theme from "theme"; +import styles from "./RoutedTabsHeader.module.css"; -export const RoutedTab = (path, link) => ({ path, link }); +export const RoutedTab = (path, link, className, activeClassName) => ({ + path, + link, + className, + activeClassName +}); -const RoutedTabsHeader = ({ tabs, caret }) => { - const nodes = useRef(new Map()); +const RoutedTabsHeader = ({ tabs, tabsClassName, caret }) => { + const { uiAnimations, caretLeft, caretWidth, nodes } = useRoutedTabsHeader(); - const [caretLeft, setCaretLeft] = useState(null); - const [caretWidth, setCaretWidth] = useState(null); - const [selectedTab, setSelectedTab] = useState(null); - const [localSidebarOnBottom, setLocalSidebarOnBottom] = useState(null); - - const { location, uiAnimations, sidebarOnBottom } = useRoutedTabsHeader(); - - const getAnimatedCaret = () => { + const getAnimatedCaret = useCallback(() => { const caretStyle = { left: spring(caretLeft, theme("springs.tab")), width: spring(caretWidth, theme("springs.tab")) @@ -25,78 +25,51 @@ const RoutedTabsHeader = ({ tabs, caret }) => { return ( {(style) => ( -
-
+
+
)} ); - }; + }, [caretLeft, caretWidth]); - const getStaticCaret = () => { + const getStaticCaret = useCallback(() => { const style = { left: caretLeft, width: caretWidth }; return ( -
-
+
+
); - }; + }, [caretLeft, caretWidth]); const tabLinks = useMemo( () => - tabs.map((t) => ( + tabs.map(({ path, link, className, activeClassName }) => ( nodes.current.set(t.path, ref)}> - - {t.link} + className={classNames(styles.tab, className)} + key={path} + ref={(ref) => nodes.current.set(path, ref)}> + + {link} )), - [tabs] + [tabs, nodes] ); - const localCaret = uiAnimations ? getAnimatedCaret() : getStaticCaret(); - - const updateCaretPosition = useCallback(() => { - const selectedTab = location.pathname; - const tabForRoute = nodes.current.get(selectedTab); - if (!tabForRoute) return null; - const tabRect = tabForRoute.getBoundingClientRect(); - const caretLeft = tabForRoute.offsetLeft; - const caretWidth = tabRect.width; - setCaretLeft(caretLeft); - setCaretWidth(caretWidth); - setSelectedTab(selectedTab); - }, [location]); - - useEffect(() => { - setLocalSidebarOnBottom(sidebarOnBottom); - updateCaretPosition(); - }, [sidebarOnBottom, updateCaretPosition]); - - useEffect(() => { - if ( - selectedTab != location.pathname || - localSidebarOnBottom != sidebarOnBottom - ) { - updateCaretPosition(); - } - }, [ - location, - selectedTab, - sidebarOnBottom, - localSidebarOnBottom, - updateCaretPosition - ]); + const localCaret = useMemo( + () => (uiAnimations ? getAnimatedCaret() : getStaticCaret()), + [uiAnimations, getAnimatedCaret, getStaticCaret] + ); return ( -
+
{tabLinks} {caret ? caret : localCaret}
diff --git a/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.module.css b/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.module.css new file mode 100644 index 0000000000..6dd4798205 --- /dev/null +++ b/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.module.css @@ -0,0 +1,58 @@ +.tabs { + position: absolute; + bottom: 0; + left: 100px; + white-space: nowrap; +} + +.tab { + line-height: 35px; + margin-right: 55px; + text-align: center; + vertical-align: middle; +} + +.tab a { + margin: 0 5px 0 5px; + color: var(--tab-text); + text-decoration: none; +} + +.tab a:hover, +.tab a.active { + color: var(--tab-text-active-hover); +} + +.tab:last-of-type { + margin-right: 0; +} + +.tabCaret { + position: absolute; + bottom: 0; + height: 5px; +} + +.tabCaret .active { + background-color: var(--accent-color); + position: absolute; + height: 5px; +} + +@media screen and (max-width: 1179px) { + .tabs { + left: 55px; + } +} + +@media screen and (max-width: 768px) { + .tabs { + font-size: 13px; + left: 25px; + right: 20px; + } + + .tab { + margin-right: 45px; + } +} diff --git a/app/components/shared/RoutedTabsHeader/hooks.js b/app/components/shared/RoutedTabsHeader/hooks.js index 29ff6f4f4b..a17c7532d3 100644 --- a/app/components/shared/RoutedTabsHeader/hooks.js +++ b/app/components/shared/RoutedTabsHeader/hooks.js @@ -1,14 +1,45 @@ +import { useEffect, useCallback, useState, useRef } from "react"; import { useSelector } from "react-redux"; import * as sel from "selectors"; export function useRoutedTabsHeader() { - const location = useSelector(sel.location); + const nodes = useRef(new Map()); + const [caretLeft, setCaretLeft] = useState(null); + const [caretWidth, setCaretWidth] = useState(null); + const [selectedTab, setSelectedTab] = useState(null); + const [localSidebarOnBottom, setLocalSidebarOnBottom] = useState(null); + const { pathname } = useSelector(sel.location); const sidebarOnBottom = useSelector(sel.sidebarOnBottom); const uiAnimations = useSelector(sel.uiAnimations); - return { - location, + const updateCaretPosition = useCallback(() => { + const selectedTab = pathname; + const tabForRoute = nodes.current.get(selectedTab); + if (!tabForRoute) return null; + const tabRect = tabForRoute.getBoundingClientRect(); + const caretLeft = tabForRoute.offsetLeft; + const caretWidth = tabRect.width; + setCaretLeft(caretLeft); + setCaretWidth(caretWidth); + setSelectedTab(selectedTab); + }, [pathname]); + + useEffect(() => { + setLocalSidebarOnBottom(sidebarOnBottom); + updateCaretPosition(); + }, [sidebarOnBottom, updateCaretPosition]); + + useEffect(() => { + if (selectedTab != pathname || localSidebarOnBottom != sidebarOnBottom) { + updateCaretPosition(); + } + }, [ + pathname, + selectedTab, sidebarOnBottom, - uiAnimations - }; + localSidebarOnBottom, + updateCaretPosition + ]); + + return { uiAnimations, caretLeft, caretWidth, nodes }; } diff --git a/app/components/views/FatalErrorPage/FatalErrorPage.js b/app/components/views/FatalErrorPage/FatalErrorPage.jsx similarity index 88% rename from app/components/views/FatalErrorPage/FatalErrorPage.js rename to app/components/views/FatalErrorPage/FatalErrorPage.jsx index 154121c86f..c1d457d3d8 100644 --- a/app/components/views/FatalErrorPage/FatalErrorPage.js +++ b/app/components/views/FatalErrorPage/FatalErrorPage.jsx @@ -4,7 +4,7 @@ import { CopyToClipboard, ExternalLink } from "shared"; import { DIFF_CONNECTION_ERROR } from "constants"; import { getAppDataDirectory } from "main_dev/paths.js"; import { useFatalErrorPage } from "./hooks"; -import "style/Layout.less"; +import styles from "./FatalErrorPage.module.css"; // right now we need to add logs by hand. It would be good having a better way // of recognizing errors. @@ -51,14 +51,14 @@ function FatalErrorPage() { daemonError.indexOf(checkSumError) !== -1: errorMessage = ( <> -
+
} @@ -98,16 +98,16 @@ function FatalErrorPage() { }; return ( -
-
-
+
+
+
:
-
-
+
+
{daemonError && ( <> -
+