diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 3945befb73..c26336c18b 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -19,27 +19,30 @@ import PropTypes from 'prop-types'; import { Link, makeStyles, Typography } from '@material-ui/core'; import HomeIcon from '@material-ui/icons/Home'; import ExploreIcon from '@material-ui/icons/Explore'; -import AccountCircle from '@material-ui/icons/AccountCircle'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import AccountTreeIcon from '@material-ui/icons/AccountTree'; + import { Sidebar, SidebarPage, sidebarConfig, SidebarContext, SidebarItem, - SidebarSpacer, SidebarDivider, + SidebarSearchField, SidebarSpace, + SidebarUserBadge, SidebarThemeToggle, } from '@backstage/core'; const useSidebarLogoStyles = makeStyles({ root: { - height: sidebarConfig.drawerWidthClosed, + width: sidebarConfig.drawerWidthClosed, + height: 3 * sidebarConfig.logoHeight, display: 'flex', flexFlow: 'row nowrap', alignItems: 'center', + marginBottom: -14, }, logoContainer: { width: sidebarConfig.drawerWidthClosed, @@ -48,9 +51,9 @@ const useSidebarLogoStyles = makeStyles({ justifyContent: 'center', }, title: { - fontSize: 24, + fontSize: sidebarConfig.logoHeight, fontWeight: 'bold', - marginLeft: 22, + marginLeft: 20, whiteSpace: 'nowrap', color: '#fff', }, @@ -61,7 +64,7 @@ const useSidebarLogoStyles = makeStyles({ const SidebarLogo: FC<{}> = () => { const classes = useSidebarLogoStyles(); - const isOpen = useContext(SidebarContext); + const { isOpen } = useContext(SidebarContext); return (
@@ -75,21 +78,30 @@ const SidebarLogo: FC<{}> = () => { ); }; +const handleSearch = (query: string): void => { + // XXX (@koroeskohr): for testing purposes + // eslint-disable-next-line no-console + console.log(query); +}; + const Root: FC<{}> = ({ children }) => ( - + + {/* Global nav, not org-specific */} + {/* End global nav */} - + + {children} diff --git a/packages/core/src/layout/Sidebar/Bar.tsx b/packages/core/src/layout/Sidebar/Bar.tsx index b32101a270..a03a33ce09 100644 --- a/packages/core/src/layout/Sidebar/Bar.tsx +++ b/packages/core/src/layout/Sidebar/Bar.tsx @@ -20,7 +20,7 @@ import React, { FC, useRef, useState } from 'react'; import { sidebarConfig, SidebarContext } from './config'; import { BackstageTheme } from '@backstage/theme'; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles((theme) => ({ root: { zIndex: 1000, position: 'relative', @@ -115,7 +115,7 @@ export const Sidebar: FC = ({ onBlur={handleClose} data-testid="sidebar-root" > - +
(theme => ({ + introCard: { + color: '#b5b5b5', + // XXX (@koroeskohr): should I be using a Mui theme variable? + fontSize: 12, + width: sidebarConfig.drawerWidthOpen, + marginTop: 18, + marginBottom: 12, + paddingLeft: sidebarConfig.iconPadding, + paddingRight: sidebarConfig.iconPadding, + }, + introDismiss: { + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'center', + marginTop: 12, + }, + introDismissLink: { + color: '#dddddd', + display: 'flex', + alignItems: 'center', + marginBottom: 4, + '&:hover': { + color: theme.palette.linkHover, + transition: theme.transitions.create('color', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.shortest, + }), + }, + }, + introDismissText: { + fontSize: '0.7rem', + fontWeight: 'bold', + textTransform: 'uppercase', + letterSpacing: 1, + }, + introDismissIcon: { + width: 18, + height: 18, + marginRight: 12, + }, +})); + +type IntroCardProps = { + text: string; + onClose: () => void; +}; + +export const IntroCard: FC = props => { + const classes = useStyles(); + const { text, onClose } = props; + const handleClose = () => onClose(); + + return ( +
+ {text} +
+ + + + Dismiss + + +
+
+ ); +}; + +type SidebarIntroLocalStorage = { + starredItemsDismissed: boolean; + recentlyViewedItemsDismissed: boolean; +}; + +type SidebarIntroCardProps = { + text: string; + onDismiss: () => void; +}; + +const SidebarIntroCard: FC = props => { + const {text, onDismiss} = props + const [collapsing, setCollapsing] = useState(false) + const startDismissing = () => { + setCollapsing(true) + } + return ( + + + + ); +}; + +const starredIntroText = `Fun fact! As you explore all the awesome plugins in Backstage, you can actually pin them to this side nav. +Keep an eye out for the little star icon (⭐) next to the plugin name and give it a click!`; +const recentlyViewedIntroText = + 'And your recently viewed plugins will pop up here!'; + +export const SidebarIntro: FC = () => { + const { isOpen } = useContext(SidebarContext); + const [ + { starredItemsDismissed, recentlyViewedItemsDismissed }, + setDismissedIntro, + ] = useLocalStorage(SIDEBAR_INTRO_LOCAL_STORAGE, { + starredItemsDismissed: false, + recentlyViewedItemsDismissed: false, + }); + + const dismissStarred = () => { + setDismissedIntro(state => ({ ...state, starredItemsDismissed: true })); + }; + const dismissRecentlyViewed = () => { + setDismissedIntro(state => ({ + ...state, + recentlyViewedItemsDismissed: true, + })); + }; + + if (!isOpen) { + return null; + } + + return ( + <> + {!starredItemsDismissed && ( + <> + + + + )} + {!recentlyViewedItemsDismissed && ( + + )} + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx index 62f02ca58b..60132f0bc9 100644 --- a/packages/core/src/layout/Sidebar/Items.tsx +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -14,87 +14,196 @@ * limitations under the License. */ -import { Link, makeStyles, styled, Theme, Typography } from '@material-ui/core'; +import { + makeStyles, + styled, + TextField, + Theme, + Typography, + Badge, +} from '@material-ui/core'; +import SearchIcon from '@material-ui/icons/Search'; import clsx from 'clsx'; -import React, { FC, useContext } from 'react'; +import React, { FC, useContext, useState, KeyboardEventHandler } from 'react'; +import { NavLink } from 'react-router-dom'; import { sidebarConfig, SidebarContext } from './config'; import { IconComponent } from '../../icons'; -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', - }, -})); +const useStyles = makeStyles((theme) => { + const { + selectedIndicatorWidth, + drawerWidthClosed, + drawerWidthOpen, + iconContainerWidth, + iconSize, + } = sidebarConfig; + + return { + root: { + color: '#b5b5b5', + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + height: 48, + cursor: 'pointer', + }, + closed: { + width: drawerWidthClosed, + justifyContent: 'center', + }, + open: { + width: drawerWidthOpen, + }, + label: { + // XXX (@koroeskohr): I can't seem to achieve the desired font-weight from the designs + fontWeight: 'bold', + whiteSpace: 'nowrap', + lineHeight: 1.0, + }, + iconContainer: { + boxSizing: 'border-box', + height: '100%', + width: iconContainerWidth, + marginRight: -theme.spacing(2), + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + icon: { + width: iconSize, + height: iconSize, + }, + searchRoot: { + marginBottom: 12, + }, + searchField: { + color: '#b5b5b5', + fontWeight: 'bold', + fontSize: theme.typography.fontSize, + }, + searchContainer: { + width: drawerWidthOpen - iconContainerWidth, + }, + selected: { + '&$root': { + borderLeft: `solid ${selectedIndicatorWidth}px #9BF0E1`, + color: '#ffffff', + }, + '&$closed': { + width: drawerWidthClosed - selectedIndicatorWidth, + }, + '& $iconContainer': { + marginLeft: -selectedIndicatorWidth, + }, + }, + }; +}); type SidebarItemProps = { icon: IconComponent; - text: string; + text?: string; to?: string; + disableSelected?: boolean; + hasNotifications?: boolean; onClick?: () => void; }; export const SidebarItem: FC = ({ icon: Icon, text, - to, + to = '#', + disableSelected = false, + hasNotifications = false, onClick, + children, }) => { const classes = useStyles(); - const isOpen = useContext(SidebarContext); + // XXX (@koroeskohr): unsure this is optimal. But I just really didn't want to have the item component + // depend on the current location, and at least have it being optionally forced to selected. + // Still waiting on a Q answered to fine tune the implementation + const { isOpen } = useContext(SidebarContext); + + const itemIcon = ( + + + + ); if (!isOpen) { return ( - match && !disableSelected} + exact + to={to} onClick={onClick} - underline="none" > - - + {itemIcon} + ); } return ( - match && !disableSelected} + exact + to={to} onClick={onClick} - underline="none" >
- + {itemIcon}
- - {text} - - + {text && ( + + {text} + + )} + {children} + + ); +}; + +type SidebarSearchFieldProps = { + onSearch: (input: string) => void; +}; + +export const SidebarSearchField: FC = (props) => { + const [input, setInput] = useState(''); + const classes = useStyles(); + + const handleEnter: KeyboardEventHandler = (ev) => { + if (ev.key === 'Enter') { + props.onSearch(input); + } + }; + + const handleInput = (ev: React.ChangeEvent) => { + setInput(ev.target.value); + }; + + return ( +
+ + + +
); }; @@ -111,4 +220,5 @@ export const SidebarDivider = styled('hr')({ width: '100%', background: '#383838', border: 'none', + margin: 0, }); diff --git a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx new file mode 100644 index 0000000000..fc0025caca --- /dev/null +++ b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + Sidebar, + SidebarIntro, + SidebarItem, + SidebarDivider, + SidebarSearchField, + SidebarSpace, + SidebarUserBadge, +} from '.'; +import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined'; +import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; +import { MemoryRouter } from 'react-router-dom'; + +export default { + title: 'Sidebar', + component: Sidebar, + decorators: [ + (storyFn: () => JSX.Element) => ( + {storyFn()} + ), + ], +}; + +const handleSearch = (input: string) => { + // eslint-disable-next-line no-console + console.log(input); +}; + +export const SampleSidebar = () => ( + + {/* */} + + + + + + + + + + + +); diff --git a/packages/core/src/layout/Sidebar/UserBadge.tsx b/packages/core/src/layout/Sidebar/UserBadge.tsx new file mode 100644 index 0000000000..1614558ab9 --- /dev/null +++ b/packages/core/src/layout/Sidebar/UserBadge.tsx @@ -0,0 +1,72 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC, useContext } from 'react'; +import { Avatar, makeStyles, Theme, Typography } from '@material-ui/core'; +import People from '@material-ui/icons/People'; +import { sidebarConfig, SidebarContext } from './config'; +import { SidebarItem } from './Items'; + +const useStyles = makeStyles(() => { + const { drawerWidthOpen, userBadgeDiameter } = sidebarConfig; + return { + root: { + width: drawerWidthOpen, + display: 'flex', + alignItems: 'center', + color: '#b5b5b5', + paddingLeft: 18, + paddingTop: 14, + paddingBottom: 14, + }, + avatar: { + width: userBadgeDiameter, + height: userBadgeDiameter, + marginRight: 8, + }, + }; +}); + +type Props = { + imageUrl: string; + name: string; + hideName?: boolean; +}; + +export const UserBadge: FC = ({ imageUrl, name, hideName = false }) => { + const classes = useStyles(); + + return ( +
+ + {!hideName && {name}} +
+ ); +}; + +export const SidebarUserBadge: FC = () => { + const { isOpen } = useContext(SidebarContext); + const isUserLoggedIn = false; + return isUserLoggedIn ? ( + + ) : ( + + ); +}; diff --git a/packages/core/src/layout/Sidebar/config.ts b/packages/core/src/layout/Sidebar/config.ts index 7695e695d7..e101153bfb 100644 --- a/packages/core/src/layout/Sidebar/config.ts +++ b/packages/core/src/layout/Sidebar/config.ts @@ -16,11 +16,34 @@ import { createContext } from 'react'; +const drawerWidthClosed = 72; +const iconPadding = 24; +const userBadgePadding = 18; + export const sidebarConfig = { - drawerWidthClosed: 64, - drawerWidthOpen: 220, - defaultOpenDelayMs: 400, - defaultCloseDelayMs: 200, + drawerWidthClosed, + drawerWidthOpen: 224, + // As per NN/g's guidance on timing for exposing hidden content + // See https://www.nngroup.com/articles/timing-exposing-content/ + defaultOpenDelayMs: 300, + defaultCloseDelayMs: 0, + defaultFadeDuration: 200, + logoHeight: 32, + iconContainerWidth: drawerWidthClosed, + iconSize: drawerWidthClosed - iconPadding * 2, + iconPadding, + selectedIndicatorWidth: 3, + userBadgePadding, + userBadgeDiameter: drawerWidthClosed - userBadgePadding * 2, }; -export const SidebarContext = createContext(false); +export const SIDEBAR_INTRO_LOCAL_STORAGE = + '@backstage/core/sidebar-intro-dismissed'; + +type SidebarContextType = { + isOpen: boolean; +}; + +export const SidebarContext = createContext({ + isOpen: false, +}); diff --git a/packages/core/src/layout/Sidebar/index.ts b/packages/core/src/layout/Sidebar/index.ts index f3f1a8872b..731d117ce3 100644 --- a/packages/core/src/layout/Sidebar/index.ts +++ b/packages/core/src/layout/Sidebar/index.ts @@ -17,5 +17,7 @@ export * from './Bar'; export * from './Page'; export * from './Items'; +export * from './Intro'; +export * from './UserBadge'; export * from './config'; export { SidebarThemeToggle } from './SidebarThemeToggle'; diff --git a/yarn.lock b/yarn.lock index 281d2fa491..4d4f3a23da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4327,9 +4327,9 @@ pretty-format "^25.1.0" "@types/testing-library__jest-dom@^5.0.4": - version "5.0.4" - resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.0.4.tgz#c7bfbafb920cd1ce40506474e70ee73637f33701" - integrity sha512-Ns69aaNvlxvXkPxIwsqeaWH5vJpwa/pdBIlf8LGkRnbV3tiqUgifs13moLXg1NQ2AM23qRR5CtHarNshvRyEdA== + version "5.6.0" + resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.6.0.tgz#325e97aacb7e4a66693e7face8a2c04f936f4a4b" + integrity sha512-VRl4kIzvtySjscCpMul3mz0UgEd40nG/jWluaXIYi5UG8cOOLD56u8IIgHZk+gSKmccRCsVv7AAg1HBmE7OQ2w== dependencies: "@types/jest" "*"