Restructure the app sidebar + shell to be mainly in core

This commit is contained in:
Fredrik Adelöw
2020-03-09 10:28:38 +01:00
parent eebc8181f3
commit 3c1ce0a6dd
11 changed files with 325 additions and 296 deletions
+6 -26
View File
@@ -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 (
<div className={classes.root}>
<SideBar />
{children}
</div>
);
};
const app = createApp();
app.registerEntityKind(...entities);
app.registerPlugin(...Object.values(plugins));
const AppComponent = app.build();
const App: FC<{}> = () => {
useStyles();
return (
<CssBaseline>
<ThemeProvider theme={BackstageTheme}>
<Router>
<AppShell>
<Root>
<AppComponent />
</AppShell>
</Root>
</Router>
</ThemeProvider>
</CssBaseline>
+70
View File
@@ -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 (
<div className={classes.root}>
<Link href="/" underline="none">
<Typography variant="h6" color="inherit" className={classes.title}>
{isOpen ? 'Backstage' : 'B'}
<span className={classes.titleDot}>.</span>
</Typography>
</Link>
</div>
);
};
const Root: FC<{}> = ({ children }) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
<SidebarSpacer />
<SidebarDivider />
<SidebarItem icon={HomeIcon} to="/" text="Home" />
<SidebarDivider />
<SidebarSpace />
</Sidebar>
{children}
</SidebarPage>
);
export default Root;
@@ -0,0 +1 @@
export { default } from './Root';
@@ -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<boolean>(false);
const useSidebarItemStyles = makeStyles<Theme>(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<SidebarItemProps> = ({
icon: Icon,
text,
to,
onClick,
}) => {
const classes = useSidebarItemStyles();
const open = useContext(Context);
if (!open) {
return (
<Link
className={clsx(classes.root, classes.closed)}
href={to}
onClick={onClick}
underline="none"
>
<Icon fontSize="small" />
</Link>
);
}
return (
<Link
className={clsx(classes.root, classes.open)}
href={to}
onClick={onClick}
underline="none"
>
<div className={classes.iconContainer}>
<Icon fontSize="small" />
</div>
<Typography variant="subtitle1" className={classes.label}>
{text}
</Typography>
</Link>
);
};
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 (
<div className={classes.root}>
<Link href="/" underline="none">
<Typography variant="h6" color="inherit" className={classes.title}>
{open ? 'Backstage' : 'B'}
<span className={classes.titleDot}>.</span>
</Typography>
</Link>
</div>
);
};
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<Props> = ({
openDelayMs = defaultOpenDelayMs,
closeDelayMs = defaultCloseDelayMs,
}) => {
const classes = useStyles();
const [state, setState] = useState(State.Closed);
const hoverTimerRef = useRef<NodeJS.Timer>();
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 (
<div
className={classes.root}
onMouseEnter={handleOpen}
onFocus={handleOpen}
onMouseLeave={handleClose}
onBlur={handleClose}
data-testid="sidebar-root"
>
<Context.Provider value={state === State.Open}>
<div
className={clsx(classes.drawer, {
[classes.drawerPeek]: state === State.Peek,
[classes.drawerOpen]: state === State.Open,
})}
>
<SidebarLogo />
<Spacer />
<Divider />
<SidebarItem icon={HomeIcon} to="/" text="Home" />
<Divider />
<Space />
</div>
</Context.Provider>
</div>
);
};
export default SideBar;
@@ -1 +0,0 @@
export { default } from './SideBar';
+1
View File
@@ -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';
+113
View File
@@ -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<Props> = ({
openDelayMs = sidebarConfig.defaultOpenDelayMs,
closeDelayMs = sidebarConfig.defaultCloseDelayMs,
children,
}) => {
const classes = useStyles();
const [state, setState] = useState(State.Closed);
const hoverTimerRef = useRef<NodeJS.Timer>();
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 (
<div
className={classes.root}
onMouseEnter={handleOpen}
onFocus={handleOpen}
onMouseLeave={handleClose}
onBlur={handleClose}
data-testid="sidebar-root"
>
<SidebarContext.Provider value={state === State.Open}>
<div
className={clsx(classes.drawer, {
[classes.drawerPeek]: state === State.Peek,
[classes.drawerOpen]: state === State.Open,
})}
>
{children}
</div>
</SidebarContext.Provider>
</div>
);
};
+104
View File
@@ -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>(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<SidebarItemProps> = ({
icon: Icon,
text,
to,
onClick,
}) => {
const classes = useStyles();
const isOpen = useContext(SidebarContext);
if (!isOpen) {
return (
<Link
className={clsx(classes.root, classes.closed)}
href={to}
onClick={onClick}
underline="none"
>
<Icon fontSize="small" />
</Link>
);
}
return (
<Link
className={clsx(classes.root, classes.open)}
href={to}
onClick={onClick}
underline="none"
>
<div className={classes.iconContainer}>
<Icon fontSize="small" />
</div>
<Typography variant="subtitle1" className={classes.label}>
{text}
</Typography>
</Link>
);
};
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',
});
+16
View File
@@ -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 <div className={classes.root}>{children}</div>;
};
@@ -0,0 +1,10 @@
import { createContext } from 'react';
export const sidebarConfig = {
drawerWidthClosed: 64,
drawerWidthOpen: 220,
defaultOpenDelayMs: 400,
defaultCloseDelayMs: 200,
};
export const SidebarContext = createContext<boolean>(false);
@@ -0,0 +1,4 @@
export * from './Bar';
export * from './Page';
export * from './Items';
export * from './config';