diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index ad6cd58cd6..17aa13599b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -1,10 +1,10 @@ -import { BackstageTheme, createApp } from '@spotify-backstage/core'; import { CssBaseline, makeStyles, ThemeProvider } from '@material-ui/core'; +import { BackstageTheme, createApp } from '@spotify-backstage/core'; import React, { FC } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; -import * as plugins from './plugins'; -import SideBar from './components/SideBar'; +import Root from './components/Root'; import entities from './entities'; +import * as plugins from './plugins'; const useStyles = makeStyles(theme => ({ '@global': { @@ -22,42 +22,22 @@ const useStyles = makeStyles(theme => ({ textDecoration: 'none', }, }, - root: { - display: 'grid', - // FIXME: Don't used a fixed width here - gridTemplateColumns: '64px auto', - gridTemplateRows: '1fr', - width: '100%', - height: '100%', - }, })); -const AppShell: FC<{}> = ({ children }) => { - const classes = useStyles(); - - return ( -
- - {children} -
- ); -}; - const app = createApp(); - app.registerEntityKind(...entities); app.registerPlugin(...Object.values(plugins)); - const AppComponent = app.build(); const App: FC<{}> = () => { + useStyles(); return ( - + - + diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx new file mode 100644 index 0000000000..a5d6c4c338 --- /dev/null +++ b/packages/app/src/components/Root/Root.tsx @@ -0,0 +1,70 @@ +import { Link, makeStyles, Typography } from '@material-ui/core'; +import HomeIcon from '@material-ui/icons/Home'; +import { + Sidebar, + SidebarPage, + sidebarConfig, + SidebarContext, + SidebarItem, + SidebarSpacer, + SidebarDivider, + SidebarSpace, +} from '@spotify-backstage/core'; +import React, { FC, useContext } from 'react'; + +const useSidebarLogoStyles = makeStyles({ + root: { + height: sidebarConfig.drawerWidthClosed, + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + }, + logoContainer: { + width: sidebarConfig.drawerWidthClosed, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + title: { + fontSize: 24, + fontWeight: 'bold', + marginLeft: 22, + whiteSpace: 'nowrap', + color: '#fff', + }, + titleDot: { + color: '#1DB954', + }, +}); + +const SidebarLogo: FC<{}> = () => { + const classes = useSidebarLogoStyles(); + const isOpen = useContext(SidebarContext); + + return ( +
+ + + {isOpen ? 'Backstage' : 'B'} + . + + +
+ ); +}; + +const Root: FC<{}> = ({ children }) => ( + + + + + + + + + + {children} + +); + +export default Root; diff --git a/packages/app/src/components/Root/index.ts b/packages/app/src/components/Root/index.ts new file mode 100644 index 0000000000..7ee9fa90d7 --- /dev/null +++ b/packages/app/src/components/Root/index.ts @@ -0,0 +1 @@ +export { default } from './Root'; diff --git a/packages/app/src/components/SideBar/SideBar.tsx b/packages/app/src/components/SideBar/SideBar.tsx deleted file mode 100644 index 92e17d6eb1..0000000000 --- a/packages/app/src/components/SideBar/SideBar.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import React, { FC, useRef, useState, createContext, useContext } from 'react'; -import clsx from 'clsx'; -import { - makeStyles, - SvgIcon, - Typography, - Theme, - Link, - styled, -} from '@material-ui/core'; - -import HomeIcon from '@material-ui/icons/Home'; - -const drawerWidthClosed = 64; -const drawerWidthOpen = 220; - -const defaultOpenDelayMs = 400; -const defaultCloseDelayMs = 200; - -const Context = createContext(false); - -const useSidebarItemStyles = makeStyles(theme => ({ - root: { - color: '#b5b5b5', - display: 'flex', - flexFlow: 'row nowrap', - alignItems: 'center', - height: 40, - cursor: 'pointer', - }, - closed: { - width: drawerWidthClosed, - justifyContent: 'center', - }, - open: { - width: drawerWidthOpen, - }, - label: { - fontWeight: 'bolder', - whiteSpace: 'nowrap', - lineHeight: 1.0, - marginLeft: theme.spacing(1), - }, - iconContainer: { - height: '100%', - width: drawerWidthClosed, - marginRight: -theme.spacing(2), - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }, -})); - -type SidebarItemProps = { - icon: typeof SvgIcon; - text: string; - to?: string; - onClick?: () => void; -}; - -const SidebarItem: FC = ({ - icon: Icon, - text, - to, - onClick, -}) => { - const classes = useSidebarItemStyles(); - const open = useContext(Context); - - if (!open) { - return ( - - - - ); - } - - return ( - -
- -
- - {text} - - - ); -}; - -const useSidebarLogoStyles = makeStyles({ - root: { - height: drawerWidthClosed, - display: 'flex', - flexFlow: 'row nowrap', - alignItems: 'center', - }, - logoContainer: { - width: drawerWidthClosed, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }, - title: { - fontSize: 24, - fontWeight: 'bold', - marginLeft: 22, - whiteSpace: 'nowrap', - color: '#fff', - }, - titleDot: { - color: '#1DB954', - }, -}); - -const SidebarLogo: FC<{}> = () => { - const classes = useSidebarLogoStyles(); - const open = useContext(Context); - - return ( -
- - - {open ? 'Backstage' : 'B'} - . - - -
- ); -}; - -const Space = styled('div')({ - flex: 1, -}); - -const Spacer = styled('div')({ - height: 8, -}); - -const Divider = styled('hr')({ - height: 1, - width: '100%', - background: '#383838', - border: 'none', -}); - -const useStyles = makeStyles(theme => ({ - root: { - zIndex: 1000, - position: 'relative', - overflow: 'visible', - width: theme.spacing(7) + 1, - }, - drawer: { - display: 'flex', - flexFlow: 'column nowrap', - alignItems: 'flex-start', - position: 'fixed', - left: 0, - top: 0, - bottom: 0, - padding: 0, - background: '#171717', - overflowX: 'hidden', - width: drawerWidthClosed, - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.shortest, - }), - }, - drawerOpen: { - width: drawerWidthOpen, - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.shorter, - }), - }, - drawerPeek: { - width: drawerWidthClosed + 4, - }, -})); - -enum State { - Closed, - Peek, - Open, -} - -type Props = { - openDelayMs?: number; - closeDelayMs?: number; -}; - -const SideBar: FC = ({ - openDelayMs = defaultOpenDelayMs, - closeDelayMs = defaultCloseDelayMs, -}) => { - const classes = useStyles(); - const [state, setState] = useState(State.Closed); - const hoverTimerRef = useRef(); - - const handleOpen = () => { - if (hoverTimerRef.current) { - clearTimeout(hoverTimerRef.current); - hoverTimerRef.current = undefined; - } - if (state !== State.Open) { - hoverTimerRef.current = setTimeout(() => { - hoverTimerRef.current = undefined; - setState(State.Open); - }, openDelayMs); - - setState(State.Peek); - } - }; - - const handleClose = () => { - if (hoverTimerRef.current) { - clearTimeout(hoverTimerRef.current); - hoverTimerRef.current = undefined; - } - if (state === State.Peek) { - setState(State.Closed); - } else if (state === State.Open) { - hoverTimerRef.current = setTimeout(() => { - hoverTimerRef.current = undefined; - setState(State.Closed); - }, closeDelayMs); - } - }; - - return ( -
- -
- - - - - - -
-
-
- ); -}; - -export default SideBar; diff --git a/packages/app/src/components/SideBar/index.ts b/packages/app/src/components/SideBar/index.ts deleted file mode 100644 index c90236635b..0000000000 --- a/packages/app/src/components/SideBar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './SideBar'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3db642aad3..41f193853d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -11,6 +11,7 @@ export { default as Header } from './layout/Header/Header'; export { default as HeaderLabel } from './layout/HeaderLabel'; export { default as InfoCard } from './layout/InfoCard'; export { default as ErrorBoundary } from './layout/ErrorBoundary'; +export * from './layout/Sidebar'; export { default as BackstageTheme } from './theme/BackstageTheme'; export { COLORS } from './theme/BackstageTheme'; export { default as HorizontalScrollGrid } from './components/HorizontalScrollGrid'; diff --git a/packages/core/src/layout/Sidebar/Bar.tsx b/packages/core/src/layout/Sidebar/Bar.tsx new file mode 100644 index 0000000000..c642c32512 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Bar.tsx @@ -0,0 +1,113 @@ +import { makeStyles } from '@material-ui/core'; +import clsx from 'clsx'; +import React, { FC, useRef, useState } from 'react'; +import { sidebarConfig, SidebarContext } from './config'; + +const useStyles = makeStyles(theme => ({ + root: { + zIndex: 1000, + position: 'relative', + overflow: 'visible', + width: theme.spacing(7) + 1, + }, + drawer: { + display: 'flex', + flexFlow: 'column nowrap', + alignItems: 'flex-start', + position: 'fixed', + left: 0, + top: 0, + bottom: 0, + padding: 0, + background: '#171717', + overflowX: 'hidden', + width: sidebarConfig.drawerWidthClosed, + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.shortest, + }), + }, + drawerOpen: { + width: sidebarConfig.drawerWidthOpen, + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.shorter, + }), + }, + drawerPeek: { + width: sidebarConfig.drawerWidthClosed + 4, + }, +})); + +enum State { + Closed, + Peek, + Open, +} + +type Props = { + openDelayMs?: number; + closeDelayMs?: number; +}; + +export const Sidebar: FC = ({ + openDelayMs = sidebarConfig.defaultOpenDelayMs, + closeDelayMs = sidebarConfig.defaultCloseDelayMs, + children, +}) => { + const classes = useStyles(); + const [state, setState] = useState(State.Closed); + const hoverTimerRef = useRef(); + + const handleOpen = () => { + if (hoverTimerRef.current) { + clearTimeout(hoverTimerRef.current); + hoverTimerRef.current = undefined; + } + if (state !== State.Open) { + hoverTimerRef.current = setTimeout(() => { + hoverTimerRef.current = undefined; + setState(State.Open); + }, openDelayMs); + + setState(State.Peek); + } + }; + + const handleClose = () => { + if (hoverTimerRef.current) { + clearTimeout(hoverTimerRef.current); + hoverTimerRef.current = undefined; + } + if (state === State.Peek) { + setState(State.Closed); + } else if (state === State.Open) { + hoverTimerRef.current = setTimeout(() => { + hoverTimerRef.current = undefined; + setState(State.Closed); + }, closeDelayMs); + } + }; + + return ( +
+ +
+ {children} +
+
+
+ ); +}; diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx new file mode 100644 index 0000000000..a39352378c --- /dev/null +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -0,0 +1,104 @@ +import { + Link, + makeStyles, + styled, + SvgIcon, + Theme, + Typography, +} from '@material-ui/core'; +import clsx from 'clsx'; +import React, { FC, useContext } from 'react'; +import { sidebarConfig, SidebarContext } from './config'; + +const useStyles = makeStyles(theme => ({ + root: { + color: '#b5b5b5', + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + height: 40, + cursor: 'pointer', + }, + closed: { + width: sidebarConfig.drawerWidthClosed, + justifyContent: 'center', + }, + open: { + width: sidebarConfig.drawerWidthOpen, + }, + label: { + fontWeight: 'bolder', + whiteSpace: 'nowrap', + lineHeight: 1.0, + marginLeft: theme.spacing(1), + }, + iconContainer: { + height: '100%', + width: sidebarConfig.drawerWidthClosed, + marginRight: -theme.spacing(2), + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, +})); + +type SidebarItemProps = { + icon: typeof SvgIcon; + text: string; + to?: string; + onClick?: () => void; +}; + +export const SidebarItem: FC = ({ + icon: Icon, + text, + to, + onClick, +}) => { + const classes = useStyles(); + const isOpen = useContext(SidebarContext); + + if (!isOpen) { + return ( + + + + ); + } + + return ( + +
+ +
+ + {text} + + + ); +}; + +export const SidebarSpace = styled('div')({ + flex: 1, +}); + +export const SidebarSpacer = styled('div')({ + height: 8, +}); + +export const SidebarDivider = styled('hr')({ + height: 1, + width: '100%', + background: '#383838', + border: 'none', +}); diff --git a/packages/core/src/layout/Sidebar/Page.tsx b/packages/core/src/layout/Sidebar/Page.tsx new file mode 100644 index 0000000000..ee2f1077de --- /dev/null +++ b/packages/core/src/layout/Sidebar/Page.tsx @@ -0,0 +1,16 @@ +import { makeStyles } from '@material-ui/core'; +import React, { FC } from 'react'; +import { sidebarConfig } from './config'; + +const useStyles = makeStyles({ + root: { + width: '100%', + minHeight: '100%', + paddingLeft: sidebarConfig.drawerWidthClosed, + }, +}); + +export const SidebarPage: FC<{}> = ({ children }) => { + const classes = useStyles(); + return
{children}
; +}; diff --git a/packages/core/src/layout/Sidebar/config.ts b/packages/core/src/layout/Sidebar/config.ts new file mode 100644 index 0000000000..50762b23ae --- /dev/null +++ b/packages/core/src/layout/Sidebar/config.ts @@ -0,0 +1,10 @@ +import { createContext } from 'react'; + +export const sidebarConfig = { + drawerWidthClosed: 64, + drawerWidthOpen: 220, + defaultOpenDelayMs: 400, + defaultCloseDelayMs: 200, +}; + +export const SidebarContext = createContext(false); diff --git a/packages/core/src/layout/Sidebar/index.ts b/packages/core/src/layout/Sidebar/index.ts new file mode 100644 index 0000000000..1c729891a2 --- /dev/null +++ b/packages/core/src/layout/Sidebar/index.ts @@ -0,0 +1,4 @@ +export * from './Bar'; +export * from './Page'; +export * from './Items'; +export * from './config';