Implement Expedias new Scalable Side Nav design - items with submenus and expand on click

Signed-off-by: hiba-aldalaty <hibaaldalaty@gmail.com>
This commit is contained in:
hiba-aldalaty
2021-10-08 17:08:03 +01:00
parent ce6387a40b
commit ea0f921cd3
16 changed files with 834 additions and 74 deletions
@@ -40,6 +40,7 @@ import {
SidebarDivider,
SidebarSpace,
SidebarScrollWrapper,
SidebarExpandButton,
} from '@backstage/core-components';
const useSidebarLogoStyles = makeStyles({
@@ -98,6 +99,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarDivider />
<Shortcuts />
<SidebarSpace />
<SidebarExpandButton />
<SidebarDivider />
<SidebarSettings />
</Sidebar>
@@ -16,10 +16,17 @@
import { makeStyles, useMediaQuery } from '@material-ui/core';
import clsx from 'clsx';
import React, { useRef, useState, useContext, PropsWithChildren } from 'react';
import React, {
useState,
useContext,
PropsWithChildren,
useEffect,
} from 'react';
import { sidebarConfig, SidebarContext } from './config';
import { BackstageTheme } from '@backstage/theme';
import { SidebarPinStateContext } from './Page';
import DoubleArrowRight from './icons/DoubleArrowRight';
import DoubleArrowLeft from './icons/DoubleArrowLeft';
export type SidebarClassKey = 'root' | 'drawer' | 'drawerOpen';
@@ -45,7 +52,6 @@ const useStyles = makeStyles<BackstageTheme>(
msOverflowStyle: 'none',
scrollbarWidth: 'none',
width: sidebarConfig.drawerWidthClosed,
borderRight: `1px solid #383838`,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.shortest,
@@ -57,6 +63,19 @@ const useStyles = makeStyles<BackstageTheme>(
display: 'none',
},
},
expandButton: {
background: 'none',
border: 'none',
color: theme.palette.navigation.color,
width: '100%',
cursor: 'pointer',
position: 'relative',
height: 48,
},
arrows: {
position: 'absolute',
right: 10,
},
drawerOpen: {
width: sidebarConfig.drawerWidthOpen,
transition: theme.transitions.create('width', {
@@ -68,81 +87,24 @@ const useStyles = makeStyles<BackstageTheme>(
{ name: 'BackstageSidebar' },
);
enum State {
Closed,
Idle,
Open,
}
type Props = {
openDelayMs?: number;
closeDelayMs?: number;
};
export function Sidebar(props: PropsWithChildren<Props>) {
const {
openDelayMs = sidebarConfig.defaultOpenDelayMs,
closeDelayMs = sidebarConfig.defaultCloseDelayMs,
children,
} = props;
export function Sidebar({ children }: PropsWithChildren<{}>) {
const classes = useStyles();
const isSmallScreen = useMediaQuery<BackstageTheme>(theme =>
theme.breakpoints.down('md'),
);
const [state, setState] = useState(State.Closed);
const hoverTimerRef = useRef<number>();
const { isPinned } = useContext(SidebarPinStateContext);
const [isOpen, setIsOpen] = useState(isPinned);
const handleOpen = () => {
useEffect(() => {
if (isPinned) {
return;
setIsOpen(true);
}
if (hoverTimerRef.current) {
clearTimeout(hoverTimerRef.current);
hoverTimerRef.current = undefined;
}
if (state !== State.Open && !isSmallScreen) {
hoverTimerRef.current = window.setTimeout(() => {
hoverTimerRef.current = undefined;
setState(State.Open);
}, openDelayMs);
setState(State.Idle);
}
};
const handleClose = () => {
if (isPinned) {
return;
}
if (hoverTimerRef.current) {
clearTimeout(hoverTimerRef.current);
hoverTimerRef.current = undefined;
}
if (state === State.Idle) {
setState(State.Closed);
} else if (state === State.Open) {
hoverTimerRef.current = window.setTimeout(() => {
hoverTimerRef.current = undefined;
setState(State.Closed);
}, closeDelayMs);
}
};
const isOpen = (state === State.Open && !isSmallScreen) || isPinned;
}, [isPinned]);
return (
<div
className={classes.root}
onMouseEnter={handleOpen}
onFocus={handleOpen}
onMouseLeave={handleClose}
onBlur={handleClose}
data-testid="sidebar-root"
>
<div className={classes.root} data-testid="sidebar-root">
<SidebarContext.Provider
value={{
isOpen,
setIsOpen,
}}
>
<div
@@ -156,3 +118,37 @@ export function Sidebar(props: PropsWithChildren<Props>) {
</div>
);
}
export const SidebarExpandButton = () => {
const classes = useStyles();
const isSmallScreen = useMediaQuery<BackstageTheme>(theme =>
theme.breakpoints.down('md'),
);
const { isPinned } = useContext(SidebarPinStateContext);
const { isOpen, setIsOpen } = useContext(SidebarContext);
const openDelayMs = sidebarConfig.defaultOpenDelayMs;
const closeDelayMs = sidebarConfig.defaultCloseDelayMs;
const handleClick = () => {
if (isPinned || isSmallScreen) {
return;
}
const delayMs = isOpen ? openDelayMs : closeDelayMs;
setTimeout(async () => {
setIsOpen(!isOpen);
}, delayMs);
};
if (isSmallScreen || isPinned) {
return null;
}
return (
<button onClick={handleClick} className={classes.expandButton}>
<div className={classes.arrows}>
{isOpen ? <DoubleArrowLeft /> : <DoubleArrowRight />}
</div>
</button>
);
};
@@ -25,11 +25,14 @@ import {
Typography,
} from '@material-ui/core';
import { CreateCSSProperties } from '@material-ui/core/styles/withStyles';
import ArrowRightIcon from '@material-ui/icons/ArrowRight';
import SearchIcon from '@material-ui/icons/Search';
import clsx from 'clsx';
import React, {
Children,
forwardRef,
KeyboardEventHandler,
PropsWithChildren,
ReactNode,
useContext,
useState,
@@ -37,10 +40,16 @@ import React, {
import {
Link,
NavLinkProps,
resolvePath,
useLocation,
useResolvedPath,
} from 'react-router-dom';
import { sidebarConfig, SidebarContext } from './config';
import {
sidebarConfig,
SidebarContext,
ItemWithSubmenuContext,
} from './config';
import { Submenu } from './Submenu';
export type SidebarItemClassKey =
| 'root'
@@ -89,6 +98,14 @@ const useStyles = makeStyles<BackstageTheme>(
open: {
width: drawerWidthOpen,
},
highlightable: {
'&:hover': {
background: theme.palette.navigation.navItem.hoverBackground, // TODO: consider
},
},
highlighted: {
background: theme.palette.navigation.navItem.hoverBackground, // TODO: consider
},
label: {
// XXX (@koroeskohr): I can't seem to achieve the desired font-weight from the designs
fontWeight: 'bold',
@@ -127,13 +144,24 @@ const useStyles = makeStyles<BackstageTheme>(
textAlign: 'center',
marginRight: theme.spacing(1),
},
closedItemIcon: {
width: '100%',
justifyContent: 'center',
},
submenuArrow: {
position: 'absolute',
right: 0,
},
selected: {
'&$root': {
borderLeft: `solid ${selectedIndicatorWidth}px ${theme.palette.navigation.indicator}`,
color: theme.palette.navigation.selectedColor,
},
'&$closed': {
width: drawerWidthClosed - selectedIndicatorWidth,
width: drawerWidthClosed,
},
'& $closedItemIcon': {
paddingRight: selectedIndicatorWidth,
},
'& $iconContainer': {
marginLeft: -selectedIndicatorWidth,
@@ -144,10 +172,117 @@ const useStyles = makeStyles<BackstageTheme>(
{ name: 'BackstageSidebarItem' },
);
type ItemWithSubmenuProps = {
label?: string;
title?: string;
hasNotifications?: boolean;
icon: IconComponent;
children: ReactNode;
};
const ItemWithSubmenu = ({
label,
title,
hasNotifications = false,
icon: Icon,
children,
}: PropsWithChildren<ItemWithSubmenuProps>) => {
const classes = useStyles();
const [isHoveredOn, setIsHoveredOn] = useState(false);
const toPathnames: string[] = [];
let isActive;
const { pathname: locationPathname } = useLocation();
// Menu item is active if any of its children have active paths
Children.forEach(children, element => {
if (!React.isValidElement(element)) return;
if (element.props.hasDropDown && element.props.dropdownItems) {
element.props.dropdownItems.map((item: { to: string }) =>
toPathnames.push(item.to),
);
} else if (element.props.to) {
toPathnames.push(element.props.to);
}
});
toPathnames.some(to => {
const toPathname = resolvePath(to);
isActive = locationPathname === toPathname.pathname;
return isActive;
});
const handleMouseEnter = () => {
setIsHoveredOn(true);
};
const handleMouseLeave = () => {
setIsHoveredOn(false);
};
const { isOpen } = useContext(SidebarContext);
const itemIcon = (
<Badge
color="secondary"
variant="dot"
overlap="circular"
className={isOpen ? undefined : classes.closedItemIcon}
invisible={!hasNotifications}
>
<Icon fontSize="small" />
</Badge>
);
const openContent = (
<>
<div data-testid="login-button" className={classes.iconContainer}>
{itemIcon}
</div>
{label && (
<Typography variant="subtitle2" className={classes.label}>
{label}
</Typography>
)}
<div className={classes.secondaryAction}>{}</div>
</>
);
const closedContent = itemIcon;
return (
<ItemWithSubmenuContext.Provider
value={{
isHoveredOn,
setIsHoveredOn,
}}
>
<div
onMouseLeave={handleMouseLeave}
className={clsx(isHoveredOn ? classes.highlighted : undefined)}
>
<div
onMouseEnter={handleMouseEnter}
className={clsx(
classes.root,
isOpen ? classes.open : classes.closed,
isActive ? classes.selected : undefined,
classes.highlightable,
isHoveredOn ? classes.highlighted : undefined,
)}
>
{isOpen ? openContent : closedContent}
{!isHoveredOn && (
<ArrowRightIcon fontSize="small" className={classes.submenuArrow} />
)}
</div>
{isHoveredOn && <Submenu title={title}>{children}</Submenu>}
</div>
</ItemWithSubmenuContext.Provider>
);
};
type SidebarItemBaseProps = {
icon: IconComponent;
text?: string;
hasNotifications?: boolean;
hasSubMenu?: boolean;
submenuTitle?: string;
disableHighlight?: boolean;
children?: ReactNode;
className?: string;
};
@@ -222,6 +357,9 @@ export const SidebarItem = forwardRef<any, SidebarItemProps>((props, ref) => {
icon: Icon,
text,
hasNotifications = false,
hasSubMenu = false,
submenuTitle,
disableHighlight = false,
onClick,
children,
className,
@@ -239,6 +377,7 @@ export const SidebarItem = forwardRef<any, SidebarItemProps>((props, ref) => {
variant="dot"
overlap="circular"
invisible={!hasNotifications}
className={clsx(isOpen ? undefined : classes.closedItemIcon)}
>
<Icon fontSize="small" />
</Badge>
@@ -248,7 +387,7 @@ export const SidebarItem = forwardRef<any, SidebarItemProps>((props, ref) => {
const openContent = (
<>
<div data-testid="login-button" className={classes.iconContainer}>
<div data-testid="login-button" className={clsx(classes.iconContainer)}>
{itemIcon}
</div>
{text && (
@@ -269,9 +408,24 @@ export const SidebarItem = forwardRef<any, SidebarItemProps>((props, ref) => {
classes.root,
isOpen ? classes.open : classes.closed,
isButtonItem(props) && classes.buttonItem,
disableHighlight ? undefined : classes.highlightable,
),
};
// If submenu prop is true, return ItemWithSubmenu
if (hasSubMenu) {
return (
<ItemWithSubmenu
label={text}
title={submenuTitle}
icon={Icon}
hasNotifications={hasNotifications}
>
{children}
</ItemWithSubmenu>
);
}
if (isButtonItem(props)) {
return (
<button aria-label={text} {...childProps} ref={ref}>
@@ -332,7 +486,12 @@ export function SidebarSearchField(props: SidebarSearchFieldProps) {
return (
<div className={classes.searchRoot}>
<SidebarItem icon={SearchIcon} to={props.to} onClick={handleItemClick}>
<SidebarItem
icon={SearchIcon}
to={props.to}
onClick={handleItemClick}
disableHighlight
>
<TextField
placeholder="Search"
value={input}
@@ -16,16 +16,25 @@
import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline';
import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined';
import BuildRoundedIcon from '@material-ui/icons/BuildRounded';
import LibraryBooksOutlinedIcon from '@material-ui/icons/LibraryBooksOutlined';
import WebOutlinedIcon from '@material-ui/icons/WebOutlined';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import {
Sidebar,
SidebarDivider,
SidebarExpandButton,
SidebarIntro,
SidebarItem,
SidebarSearchField,
SidebarSpace,
} from '.';
import { SubmenuItem } from './SubmenuItem';
import { CatalogSidebarLogo } from './icons/CatalogSidebarLogo';
import { APIsIcon } from './icons/APIsIcon';
import { ServicesIcon } from './icons/ServicesIcon';
import { MiscIcon } from './icons/MiscIcon';
export default {
title: 'Layout/Sidebar',
@@ -46,11 +55,40 @@ export const SampleSidebar = () => (
<Sidebar>
<SidebarSearchField onSearch={handleSearch} to="/search" />
<SidebarDivider />
<SidebarItem icon={HomeOutlinedIcon} to="#" text="Home" />
<SidebarItem
icon={CatalogSidebarLogo}
onClick={() => {}}
text="Catalog"
hasSubMenu
submenuTitle="Catalog"
>
<SubmenuItem title="Tools" to="/1" icon={BuildRoundedIcon} />
<SubmenuItem title="APIs" to="/2" icon={APIsIcon} />
<SubmenuItem title="Services" to="/3" icon={ServicesIcon} />
<SubmenuItem title="Libraries" to="/4" icon={LibraryBooksOutlinedIcon} />
<SubmenuItem title="Websites" to="/5" icon={WebOutlinedIcon} />
<SubmenuItem
title="Misc"
to="/6"
icon={MiscIcon}
hasDropDown
dropdownItems={[
{
title: 'Lorem Ipsum',
to: '/7',
},
{
title: 'Lorem Ipsum',
to: '/8',
},
]}
/>
</SidebarItem>
<SidebarItem icon={HomeOutlinedIcon} to="#" text="Plugins" />
<SidebarItem icon={AddCircleOutlineIcon} to="#" text="Create..." />
<SidebarDivider />
<SidebarIntro />
<SidebarSpace />
<SidebarExpandButton />
</Sidebar>
);
@@ -0,0 +1,97 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { makeStyles, Typography } from '@material-ui/core';
import clsx from 'clsx';
import React, { PropsWithChildren, ReactNode, useContext } from 'react';
import {
ItemWithSubmenuContext,
sidebarConfig,
SidebarContext,
submenuConfig,
} from './config';
import { BackstageTheme } from '@backstage/theme';
const useStyles = (props: { left: number }) =>
makeStyles<BackstageTheme>(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: props.left,
top: 0,
bottom: 0,
padding: 0,
background: theme.palette.navigation.submenu.background,
overflowX: 'hidden',
msOverflowStyle: 'none',
scrollbarWidth: 'none',
cursor: 'default',
width: submenuConfig.drawerWidthClosed,
borderRight: `1px solid #383838`,
'& > *': {
flexShrink: 0,
},
'&::-webkit-scrollbar': {
display: 'none',
},
},
drawerOpen: {
width: submenuConfig.drawerWidthOpen,
},
title: {
fontSize: 24,
fontWeight: 500,
color: '#FFF',
padding: 20,
},
}));
type SubmenuProps = {
title?: string;
children: ReactNode;
};
export const Submenu = ({
title,
children,
}: PropsWithChildren<SubmenuProps>) => {
const { isOpen } = useContext(SidebarContext);
const left = isOpen
? sidebarConfig.drawerWidthOpen
: sidebarConfig.drawerWidthClosed;
const props = { left: left };
const classes = useStyles(props)();
const { isHoveredOn } = useContext(ItemWithSubmenuContext);
return (
<div
className={clsx(classes.drawer, {
[classes.drawerOpen]: isHoveredOn,
})}
>
<Typography variant="h5" className={classes.title}>
{title}
</Typography>
{children}
</div>
);
};
@@ -0,0 +1,170 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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, { useContext, useState } from 'react';
import {
NavLink,
resolvePath,
useLocation,
useResolvedPath,
} from 'react-router-dom';
import { Link, makeStyles, Typography } from '@material-ui/core';
import { IconComponent } from '@backstage/core-plugin-api';
import clsx from 'clsx';
import { BackstageTheme } from '@backstage/theme';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp';
import { ItemWithSubmenuContext } from './config';
const useStyles = makeStyles<BackstageTheme>(theme => ({
item: {
height: 48,
width: '100%',
'&:hover': {
background: '#6f6f6f',
color: theme.palette.navigation.selectedColor,
},
display: 'flex',
alignItems: 'center',
color: theme.palette.navigation.color,
padding: 20,
cursor: 'pointer',
position: 'relative',
background: 'none',
border: 'none',
},
itemContainer: {
width: '100%',
},
selected: {
background: '#6f6f6f',
color: '#FFF',
},
label: {
margin: 14,
marginLeft: 7,
fontSize: 14,
},
dropdownArrow: {
position: 'absolute',
right: 21,
},
dropdown: {
display: 'flex',
flexDirection: 'column',
alignItems: 'end',
},
dropdownItem: {
width: '72%',
margin: '10px 0 10px 0',
color: theme.palette.navigation.color,
},
}));
type DropDownItem = {
title: string;
to: string;
};
type SubmenuItemProps = {
title: string;
to: string;
hasDropDown?: boolean;
icon: IconComponent;
dropdownItems?: DropDownItem[];
};
export const SubmenuItem = ({
title,
to,
hasDropDown = false,
icon: Icon,
dropdownItems,
}: SubmenuItemProps) => {
const classes = useStyles();
const { pathname: locationPathname } = useLocation();
const { pathname: toPathname } = useResolvedPath(to);
const { setIsHoveredOn } = useContext(ItemWithSubmenuContext);
const closeSubmenu = () => {
setIsHoveredOn(false);
};
let isActive = locationPathname === toPathname;
const [showDropDown, setShowDropDown] = useState(false);
const handleClickDropdown = () => {
setShowDropDown(!showDropDown);
};
if (hasDropDown && dropdownItems !== undefined) {
dropdownItems.some(item => {
const resolvedPath = resolvePath(item.to);
isActive = locationPathname === resolvedPath.pathname;
return isActive;
});
return (
<div className={classes.itemContainer}>
<button
onClick={handleClickDropdown}
className={clsx(
classes.item,
isActive ? classes.selected : undefined,
)}
>
<Icon fontSize="small" />
<Typography variant="subtitle1" className={classes.label}>
{title}
</Typography>
{showDropDown ? (
<ArrowDropUpIcon className={classes.dropdownArrow} />
) : (
<ArrowDropDownIcon className={classes.dropdownArrow} />
)}
</button>
{hasDropDown && showDropDown && (
<div className={classes.dropdown}>
{dropdownItems.map((object, key) => (
<Link
component={NavLink}
to={object.to}
underline="none"
className={classes.dropdownItem}
onClick={closeSubmenu}
key={key}
>
{object.title}
</Link>
))}
</div>
)}
</div>
);
}
return (
<div className={classes.itemContainer}>
<Link
component={NavLink}
to={to}
underline="none"
className={clsx(classes.item, isActive ? classes.selected : undefined)}
onClick={closeSubmenu}
>
<Icon fontSize="small" />
<Typography variant="subtitle1" className={classes.label}>
{title}
</Typography>
</Link>
</div>
);
};
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { createContext } from 'react';
import { createContext, Dispatch, SetStateAction } from 'react';
const drawerWidthClosed = 72;
const iconPadding = 24;
@@ -37,13 +37,44 @@ export const sidebarConfig = {
userBadgeDiameter: drawerWidthClosed - userBadgePadding * 2,
};
export const submenuConfig = {
drawerWidthClosed: 0,
drawerWidthOpen: 202,
// As per NN/g's guidance on timing for exposing hidden content
// See https://www.nngroup.com/articles/timing-exposing-content/
defaultOpenDelayMs: 100,
defaultCloseDelayMs: 0,
defaultFadeDuration: 200,
logoHeight: 32,
iconContainerWidth: drawerWidthClosed,
iconSize: drawerWidthClosed - iconPadding * 2,
iconPadding,
selectedIndicatorWidth: 3,
userBadgePadding,
userBadgeDiameter: drawerWidthClosed - userBadgePadding * 2,
};
export const SIDEBAR_INTRO_LOCAL_STORAGE =
'@backstage/core/sidebar-intro-dismissed';
export type SidebarContextType = {
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
};
export const SidebarContext = createContext<SidebarContextType>({
isOpen: false,
setIsOpen: () => {},
});
export type ItemWithSubmenuContextType = {
isHoveredOn: boolean;
setIsHoveredOn: Dispatch<SetStateAction<boolean>>;
};
export const ItemWithSubmenuContext = createContext<ItemWithSubmenuContextType>(
{
isHoveredOn: false,
setIsHoveredOn: () => {},
},
);
@@ -0,0 +1,33 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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';
export const APIsIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14 12L12 14L10 12L12 10L14 12ZM12 6L14.12 8.12L16.62 5.62L12 1L7.38 5.62L9.88 8.12L12 6ZM6 12L8.12 9.88L5.62 7.38L1 12L5.62 16.62L8.12 14.12L6 12ZM18 12L15.88 14.12L18.38 16.62L23 12L18.38 7.38L15.88 9.88L18 12ZM12 18L9.88 15.88L7.38 18.38L12 23L16.62 18.38L14.12 15.88L12 18Z"
fill="#BDBDBD"
/>
</svg>
);
};
@@ -0,0 +1,40 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { SvgIcon } from '@material-ui/core';
import React from 'react';
export const CatalogSidebarLogo = () => {
return (
<SvgIcon
xmlns="http://www.w3.org/2000/svg"
width="20"
height="21"
fill="none"
viewBox="0 0 20 21"
>
<path
fillRule="evenodd"
d="M4 9.923l5.5-9 5.5 9H4zm7.43-2L9.5 4.763l-1.94 3.16h3.87zm3.57 4c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5zm-2.5 4.5a2.5 2.5 0 005 0 2.5 2.5 0 00-5 0zm-12 4h8v-8h-8v8zm6-6h-4v4h4v-4z"
clipRule="evenodd"
/>
<path
fillRule="evenodd"
d="M4 9.923l5.5-9 5.5 9H4zm7.43-2L9.5 4.763l-1.94 3.16h3.87zm3.57 4c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5zm-2.5 4.5a2.5 2.5 0 005 0 2.5 2.5 0 00-5 0zm-12 4h8v-8h-8v8zm6-6h-4v4h4v-4z"
clipRule="evenodd"
/>
</SvgIcon>
);
};
@@ -0,0 +1,50 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { makeStyles } from '@material-ui/core';
const useStyles = makeStyles({
svg: {
width: 'auto',
height: 15,
},
path: {
fill: '#7df3e1',
},
});
const DoubleArrowRight = () => {
const classes = useStyles();
return (
<svg
className={classes.svg}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
>
<path
d="M15.18 4.94408L14.236 4.00008L8.90266 9.33341L14.236 14.6667L15.18 13.7227L10.7907 9.33341L15.18 4.94408Z"
fill="#BDBDBD"
/>
<path
d="M8.90271 4.94408L7.95871 4.00008L2.62538 9.33341L7.95871 14.6667L8.90271 13.7227L4.51338 9.33341L8.90271 4.94408Z"
fill="#BDBDBD"
/>
</svg>
);
};
export default DoubleArrowRight;
@@ -0,0 +1,50 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { makeStyles } from '@material-ui/core';
const useStyles = makeStyles({
svg: {
width: 'auto',
height: 15,
},
path: {
fill: '#7df3e1',
},
});
const DoubleArrowRight = () => {
const classes = useStyles();
return (
<svg
className={classes.svg}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
>
<path
d="M0.82 11.0559L1.764 11.9999L7.09733 6.66658L1.764 1.33325L0.82 2.27725L5.20933 6.66658L0.82 11.0559Z"
fill="#BDBDBD"
/>
<path
d="M7.0973 11.0559L8.0413 11.9999L13.3746 6.66658L8.0413 1.33325L7.0973 2.27725L11.4866 6.66658L7.0973 11.0559Z"
fill="#BDBDBD"
/>
</svg>
);
};
export default DoubleArrowRight;
@@ -0,0 +1,33 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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';
export const MiscIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M21.29 13.9L18 12L21.29 10.1C21.77 9.82 21.93 9.21 21.66 8.73L19.66 5.27C19.38 4.79 18.77 4.63 18.29 4.9L15 6.8V3C15 2.45 14.55 2 14 2H9.99999C9.44999 2 8.99999 2.45 8.99999 3V6.8L5.71 4.9C5.23 4.63 4.62 4.79 4.34 5.27L2.33999 8.73C2.05999 9.21 2.22999 9.82 2.70999 10.1L5.99999 12L2.70999 13.9C2.22999 14.18 2.06999 14.79 2.33999 15.27L4.34 18.73C4.62 19.21 5.23 19.37 5.71 19.1L8.99999 17.2V21C8.99999 21.55 9.44999 22 9.99999 22H14C14.55 22 15 21.55 15 21V17.2L18.29 19.1C18.77 19.38 19.38 19.21 19.66 18.73L21.66 15.27C21.94 14.79 21.77 14.18 21.29 13.9ZM18.43 16.87L13.75 14.17C13.42 13.97 13 14.21 13 14.6V20H11V14.6C11 14.22 10.58 13.97 10.25 14.17L5.57 16.87L4.57 15.14L9.24999 12.44C9.57999 12.25 9.57999 11.77 9.24999 11.57L4.57 8.87L5.57 7.14L10.25 9.84C10.58 10.03 11 9.79 11 9.4V4H13V9.4C13 9.78 13.42 10.03 13.75 9.83L18.43 7.13L19.43 8.86L14.75 11.56C14.42 11.75 14.42 12.23 14.75 12.43L19.43 15.13L18.43 16.87Z"
fill="#BDBDBD"
/>
</svg>
);
};
@@ -0,0 +1,33 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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';
export const ServicesIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.59 6.59L14.18 5.5L16.59 4.41L17.68 2L18.77 4.41L21.18 5.5L18.77 6.59L17.68 9L16.59 6.59ZM20.46 12.72L19.68 11L18.9 12.72L17.18 13.5L18.9 14.28L19.68 16L20.46 14.28L22.18 13.5L20.46 12.72ZM15.42 14.37L17.36 15.84L14.86 20.17L12.62 19.23C12.42 19.36 12.2 19.49 11.98 19.6L11.68 22H6.68L6.38 19.59C6.16 19.48 5.95 19.36 5.74 19.22L3.5 20.16L1 15.83L2.94 14.36C2.93 14.25 2.93 14.12 2.93 14C2.93 13.88 2.93 13.75 2.94 13.63L1 12.16L3.5 7.83L5.74 8.77C5.94 8.64 6.16 8.51 6.38 8.4L6.68 6H11.68L11.98 8.41C12.2 8.52 12.41 8.64 12.62 8.78L14.86 7.84L17.36 12.17L15.42 13.64C15.43 13.76 15.43 13.88 15.43 14.01C15.43 14.14 15.43 14.25 15.42 14.37ZM12.18 14C12.18 12.34 10.84 11 9.18 11C7.52 11 6.18 12.34 6.18 14C6.18 15.66 7.52 17 9.18 17C10.84 17 12.18 15.66 12.18 14Z"
fill="#BDBDBD"
/>
</svg>
);
};
@@ -14,7 +14,8 @@
* limitations under the License.
*/
export { Sidebar } from './Bar';
export { Sidebar, SidebarExpandButton } from './Bar';
export { SubmenuItem } from './SubmenuItem';
export type { SidebarClassKey } from './Bar';
export { SidebarPage, SidebarPinStateContext } from './Page';
export type { SidebarPinStateContextType, SidebarPageClassKey } from './Page';
+18
View File
@@ -70,6 +70,15 @@ export const lightTheme = createTheme({
indicator: '#9BF0E1',
color: '#b5b5b5',
selectedColor: '#FFF',
navItem: {
hoverBackground: '#404040',
},
submenu: {
background: '#404040',
indicator: '#9BF0E1',
color: '#b5b5b5',
selectedColor: '#FFF',
},
},
pinSidebarButton: {
icon: '#181818',
@@ -139,6 +148,15 @@ export const darkTheme = createTheme({
indicator: '#9BF0E1',
color: '#b5b5b5',
selectedColor: '#FFF',
navItem: {
hoverBackground: '#404040',
},
submenu: {
background: '#404040',
indicator: '#9BF0E1',
color: '#b5b5b5',
selectedColor: '#FFF',
},
},
pinSidebarButton: {
icon: '#404040',
+9
View File
@@ -48,6 +48,15 @@ type PaletteAdditions = {
indicator: string;
color: string;
selectedColor: string;
navItem: {
hoverBackground: string;
};
submenu: {
background: string;
indicator: string;
color: string;
selectedColor: string;
};
};
tabbar: {
indicator: string;