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 (
+
+ );
+};
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';