Restructure the app sidebar + shell to be mainly in core
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user