feat: Add sidebar and panel (#866)

* feat: Add sidebar and panel

* fix: fix sidebar values
This commit is contained in:
Bruno Quaresma
2022-04-05 14:01:45 -03:00
committed by GitHub
parent abf6934ebb
commit abddd64497
8 changed files with 916 additions and 0 deletions
@@ -0,0 +1,21 @@
import { Story } from "@storybook/react"
import React from "react"
import { Panel, PanelProps } from "./"
export default {
title: "Page/Panel",
component: Panel,
}
const Template: Story<PanelProps> = (args: PanelProps) => <Panel {...args} />
export const Example = Template.bind({})
Example.args = {
title: "Panel title",
activeTab: "oauthSettings",
menuItems: [
{ label: "OAuth Settings", value: "oauthSettings" },
{ label: "Security", value: "oauthSettings", hasChanges: true },
{ label: "Hardware", value: "oauthSettings" },
],
}
+102
View File
@@ -0,0 +1,102 @@
import { makeStyles } from "@material-ui/core/styles"
import { fade } from "@material-ui/core/styles/colorManipulator"
import React from "react"
import { Sidebar, SidebarItem } from "../Sidebar"
export type AdminMenuItemCallback = (menuItem: string) => void
export interface PanelProps {
title: string
menuItems: SidebarItem[]
activeTab: string
onSelect: AdminMenuItemCallback
}
export const Panel: React.FC<PanelProps> = ({ children, title, menuItems, activeTab, onSelect }) => {
const styles = useStyles()
return (
<div className={styles.root}>
<div className={styles.inner}>
<div className={styles.menuPanel}>
<div className={styles.title}>{title}</div>
<Sidebar menuItems={menuItems} activeItem={activeTab} onSelect={onSelect} />
</div>
<div className={styles.contentPanel}>{children}</div>
</div>
</div>
)
}
const useStyles = makeStyles((theme) => ({
root: {
minHeight: 400,
marginBottom: theme.spacing(2),
// Prevents scrollbar jitter from long content
marginRight: "calc(-1 * (100vw - 100%))",
},
inner: {
display: "flex",
maxWidth: 1920,
padding: theme.spacing(5, 3.5, 0, 4),
},
icon: {
fontSize: 100,
position: "absolute",
left: -50,
top: 31,
color: fade(theme.palette.common.black, 0.1),
transition: "transform 0.3s ease",
zIndex: -1,
},
menuPanel: {
paddingRight: 40,
},
title: {
marginTop: theme.spacing(4),
fontSize: 32,
letterSpacing: -theme.spacing(0.0375),
},
contentPanel: {
display: "flex",
flexDirection: "column",
width: "100%",
maxWidth: 930,
},
[theme.breakpoints.up("lg")]: {
icon: {
position: "relative",
top: -11,
left: 30,
},
contentPanel: {
width: 930,
},
},
[theme.breakpoints.down("lg")]: {
contentPanel: {
width: 890,
},
},
[theme.breakpoints.down("md")]: {
contentPanel: {
width: 700,
},
},
[theme.breakpoints.down("sm")]: {
contentPanel: {
width: 550,
},
root: {
marginRight: 0, //disabled scrollbar jump trick to avoid small screen bug with menu
},
},
}))
@@ -0,0 +1,20 @@
import { Story } from "@storybook/react"
import React from "react"
import { Sidebar, SidebarProps } from "./"
export default {
title: "Page/Sidebar",
component: Sidebar,
}
const Template: Story<SidebarProps> = (args: SidebarProps) => <Sidebar {...args} />
export const Example = Template.bind({})
Example.args = {
activeItem: "oauthSettings",
menuItems: [
{ label: "OAuth Settings", value: "oauthSettings" },
{ label: "Security", value: "security", hasChanges: true },
{ label: "Hardware", value: "hardware" },
],
}
+104
View File
@@ -0,0 +1,104 @@
import List from "@material-ui/core/List"
import ListItem from "@material-ui/core/ListItem"
import { makeStyles } from "@material-ui/core/styles"
import React from "react"
import { combineClasses } from "../../util/combine-classes"
export interface SidebarItem {
value: string
label: string
hasChanges?: boolean
}
export interface SidebarProps {
menuItems: SidebarItem[]
activeItem?: string
onSelect?: (value: string) => void
}
export const Sidebar: React.FC<SidebarProps> = ({ menuItems, activeItem, onSelect }) => {
const styles = useStyles()
return (
<List className={styles.menu}>
{menuItems.map(({ hasChanges, ...tab }) => {
const isActive = activeItem === tab.value
return (
<ListItem
key={tab.value}
button
onClick={onSelect ? () => onSelect(tab.value) : undefined}
className={styles.menuItem}
disableRipple
focusRipple={false}
component="li"
>
<span className={combineClasses({ [styles.menuItemSpan]: true, active: isActive })}>
{hasChanges ? `${tab.label}*` : tab.label}
</span>
</ListItem>
)
})}
</List>
)
}
const useStyles = makeStyles((theme) => ({
menu: {
minWidth: 160,
marginTop: theme.spacing(5),
},
menuItem: {
letterSpacing: -theme.spacing(0.0375),
padding: 0,
fontSize: 18,
color: theme.palette.text.secondary,
"&.MuiListItem-button:hover": {
backgroundColor: "transparent",
},
},
menuItemSpan: {
paddingTop: theme.spacing(1.5),
paddingBottom: theme.spacing(1),
transition: "300ms all ease",
position: "relative",
"&:hover": {
color: theme.palette.text.primary,
},
"&.active": {
color: theme.palette.primary.dark,
},
"&.active, &:hover": {
"&::before": {
opacity: 1,
},
"&::after": {
opacity: 1,
},
},
},
[theme.breakpoints.up("lg")]: {
menuItemSpan: {
"&::before": {
content: "'{'",
position: "absolute",
left: -15,
opacity: 0,
transition: "inherit",
},
"&::after": {
content: "'}'",
position: "absolute",
right: -15,
opacity: 0,
transition: "inherit",
},
},
},
}))
+562
View File
@@ -0,0 +1,562 @@
import { darken, fade } from "@material-ui/core/styles/colorManipulator"
import { Breakpoints } from "@material-ui/core/styles/createBreakpoints"
import {
BODY_FONT_FAMILY,
borderRadius,
buttonBorderWidth,
emptyBoxShadow,
lightButtonShadow,
spacing,
} from "./constants"
import { CustomPalette } from "./palettes"
import { typography } from "./typography"
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const getOverrides = (palette: CustomPalette, breakpoints: Breakpoints) => {
const containedButtonShadow = palette.type === "dark" ? emptyBoxShadow : lightButtonShadow
return {
MuiAvatar: {
root: {
width: 32,
height: 32,
fontSize: 24,
},
},
MuiTable: {
root: {
backgroundColor: palette.background.paper,
borderRadius: borderRadius * 2,
boxShadow: `0 0 0 1px ${palette.action.hover}`,
"&.is-inline": {
background: "none",
borderRadius: 0,
boxShadow: "none",
},
},
},
MuiTableHead: {
root: {
"& .MuiTableCell-root": {
...typography.overline,
paddingTop: spacing,
paddingBottom: spacing,
color: palette.text.secondary,
backgroundColor: palette.background.default,
".MuiTable-root.is-inline &": {
background: "none",
},
},
"& .MuiTableRow-root:first-child .MuiTableCell-root:first-child": {
borderTopLeftRadius: borderRadius * 2,
".MuiTable-root.is-inline &": {
borderRadius: 0,
},
},
"& .MuiTableRow-root:first-child .MuiTableCell-root:last-child": {
borderTopRightRadius: borderRadius * 2,
".MuiTable-root.is-inline &": {
borderRadius: 0,
},
},
},
},
MuiTableSortLabel: {
root: {
lineHeight: "20px",
"&$active": {
"&& $icon": {
color: palette.text.primary,
},
},
},
icon: {
"&.MuiSvgIcon-root": {
width: 18,
height: 18,
marginLeft: 2,
},
},
},
MuiTableCell: {
root: {
"&:first-child": {
paddingLeft: spacing * 4,
},
},
},
MuiTablePagination: {
input: {
height: "auto",
marginTop: 0,
border: `1px solid ${palette.action.disabled}`,
},
select: {
paddingTop: 6,
fontSize: 14,
},
selectIcon: {
right: -4,
},
},
MuiList: {
root: {
boxSizing: "border-box",
"& .MuiDivider-root": {
opacity: 0.5,
},
},
},
MuiListItem: {
root: {
minHeight: 24,
position: "relative",
"&.active": {
color: "white",
},
},
gutters: {
padding: spacing / 2,
paddingLeft: spacing * 2,
paddingRight: spacing * 2,
},
},
MuiListItemAvatar: {
root: {
minWidth: 46,
},
},
MuiListItemIcon: {
root: {
minWidth: 36,
color: palette.text.primary,
"& .MuiSvgIcon-root": {
width: 20,
height: 20,
},
},
},
MuiListSubheader: {
root: {
...typography.overline,
lineHeight: 2.5,
},
sticky: {
backgroundColor: palette.background.paper,
},
},
MuiMenu: {
paper: {
marginTop: spacing,
},
},
MuiMenuItem: {
root: {
minHeight: 48,
fontSize: 16,
[breakpoints.up("sm")]: {
minHeight: 48,
},
},
gutters: {
padding: spacing,
paddingLeft: spacing * 2,
paddingRight: spacing * 2,
},
},
MuiListItemText: {
primary: {
fontWeight: 500,
},
secondary: {
fontSize: 12,
},
},
MuiOutlinedInput: {
root: {
fontSize: 14,
"&$focused $notchedOutline": {
borderWidth: 1,
},
},
notchedOutline: {
borderWidth: 1,
borderColor: palette.action.disabled,
},
},
MuiButton: {
root: {
minHeight: 40,
paddingLeft: 20,
paddingRight: 20,
fontWeight: 500,
"& .MuiSvgIcon-root": {
verticalAlign: "middle",
},
},
contained: {
backgroundColor: palette.hero.button,
color: palette.primary.contrastText,
boxShadow: containedButtonShadow,
"&:hover": {
backgroundColor: darken(palette.hero.button, 0.25),
},
"&$disabled": {
color: fade(palette.text.disabled, 0.5),
},
},
containedPrimary: {
boxShadow: containedButtonShadow,
},
containedSecondary: {
boxShadow: containedButtonShadow,
},
outlined: {
borderColor: palette.action.disabled,
borderWidth: buttonBorderWidth,
"&:hover": {
color: palette.primary.main,
borderColor: palette.primary.main,
borderWidth: buttonBorderWidth,
},
"&$disabled": {
color: fade(palette.text.disabled, 0.5),
},
},
outlinedPrimary: {
borderColor: palette.primary.main,
borderWidth: buttonBorderWidth,
"&:hover": {
borderWidth: buttonBorderWidth,
},
},
outlinedSecondary: {
borderColor: palette.secondary.main,
borderWidth: buttonBorderWidth,
"&:hover": {
color: palette.secondary.main,
borderWidth: buttonBorderWidth,
},
},
text: {
"&$disabled": {
color: fade(palette.text.disabled, 0.5),
},
},
sizeSmall: {
minHeight: 32,
paddingLeft: 10,
paddingRight: 10,
letterSpacing: 1.2,
fontSize: 13,
"&.MuiButton-outlined": {
borderWidth: 1,
},
},
sizeLarge: {
minHeight: 46,
paddingLeft: spacing * 3,
paddingRight: spacing * 3,
},
},
MuiButtonGroup: {
contained: {
boxShadow: containedButtonShadow,
},
},
MuiLink: {
root: {
fontWeight: 600,
},
},
MuiInputBase: {
root: {
minHeight: 40,
background: palette.background.paper,
marginTop: "12px",
borderRadius,
"&$disabled": {
background: palette.action.disabledBackground,
},
"&$focused .MuiSelect-icon": {
color: palette.primary.light,
},
},
input: {
fontSize: 16,
"&::placeholder": {
color: palette.text.secondary,
opacity: 1,
},
// See this thread https://stackoverflow.com/questions/69196525/how-to-change-the-color-of-the-calendar-icon-of-the-textfield-date-picker-in-ma
"&::-webkit-calendar-picker-indicator": {
filter:
palette.type === "dark"
? "invert(95%) sepia(0%) saturate(1082%) hue-rotate(173deg) brightness(84%) contrast(80%);"
: undefined,
},
},
},
MuiInputLabel: {
shrink: {
fontSize: 14,
marginTop: "2px",
left: "-12px",
transform: "translate(16px, -6px) scale(0.8)",
color: palette.text.primary,
},
outlined: {
letterSpacing: "0.2px",
lineHeight: "16px",
fontFamily: BODY_FONT_FAMILY,
"&$shrink": {
transform: "translate(14px, -13px)",
},
},
},
MuiChip: {
root: {
fontSize: 16,
fontWeight: 500,
borderRadius: borderRadius,
backgroundColor: fade(palette.text.secondary, 0.1),
color: palette.text.secondary,
},
sizeSmall: {
height: 20,
},
labelSmall: {
fontSize: 12,
paddingLeft: 6,
paddingRight: 6,
},
colorPrimary: {
backgroundColor: fade(palette.primary.main, 0.1),
color: palette.primary.main,
},
colorSecondary: {
backgroundColor: fade(palette.secondary.main, 0.15),
color: palette.secondary.main,
},
},
MuiSelect: {
root: {
// Matches MuiInputBase-input height
lineHeight: "1.1875em",
overflow: "hidden",
},
select: {
"&:focus": {
borderRadius,
},
},
icon: {
"&.MuiSvgIcon-root": {
fontSize: 20,
margin: "3px 6px 0 0",
},
},
iconOutlined: {
right: 0,
},
iconOpen: {
transform: "none",
},
},
MuiDialog: {
paper: {
borderRadius: 11,
},
paperWidthSm: {
maxWidth: "530px",
},
},
MuiDialogTitle: {
root: {
color: palette.text.primary,
padding: `27px ${spacing * 6}px 0 ${spacing * 6}px`,
fontSize: 32,
fontWeight: 700,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
background: palette.background.paper,
},
},
MuiDialogContent: {
root: {
padding: `0 ${spacing * 6}px ${spacing * 6}px ${spacing * 6}px`,
},
},
MuiDialogContentText: {
root: {
color: palette.action.active,
},
},
MuiDialogActions: {
root: {
padding: 0,
},
spacing: {
"& > :not(:first-child)": {
marginLeft: 0,
},
},
},
MuiFormHelperText: {
root: {
fontSize: 12,
lineHeight: "16px",
},
contained: {
marginTop: 6,
marginLeft: spacing * 0.5,
},
},
MuiBackdrop: {
root: {
backgroundColor: fade("#242424", 0.6),
},
},
MuiInputAdornment: {
root: {
"& .MuiSvgIcon-root": {
color: palette.text.secondary,
},
},
positionStart: {
marginRight: 12,
marginLeft: 6,
},
},
MuiCheckbox: {
root: {
"& .MuiSvgIcon-root": {
width: 16,
height: 16,
margin: 1,
},
},
colorPrimary: {
"&.MuiCheckbox-root": {
color: palette.primary.main,
"&$disabled": {
color: palette.action.disabled,
},
},
},
colorSecondary: {
"&.MuiCheckbox-root": {
color: palette.secondary.main,
"&$disabled": {
color: palette.action.disabled,
},
},
},
},
MuiRadio: {
root: {
"& .MuiSvgIcon-root": {
width: 18,
height: 18,
},
},
colorPrimary: {
"&.MuiRadio-root": {
color: palette.primary.main,
"&$disabled": {
color: palette.action.disabled,
},
},
},
colorSecondary: {
"&.MuiRadio-root": {
color: palette.secondary.main,
"&$disabled": {
color: palette.action.disabled,
},
},
},
},
MuiTabs: {
root: {
minHeight: 40,
},
indicator: {
height: 3,
},
},
MuiTab: {
root: {
minHeight: 70,
marginRight: spacing * 5,
textTransform: "none",
fontWeight: 400,
fontSize: 16,
letterSpacing: 3,
"&:last-child": {
marginRight: 0,
},
"&.MuiButtonBase-root": {
minWidth: 0,
padding: 0,
},
"&.Mui-selected .MuiTab-wrapper": {
color: palette.primary.contrastText,
fontWeight: "bold",
},
},
wrapper: {},
textColorPrimary: {
color: palette.text.secondary,
},
textColorSecondary: {
color: palette.text.secondary,
},
},
MuiStepper: {
root: {
background: "none",
},
},
// Labs components aren't typed properly, so cast overrides
MuiAutocomplete: {
endAdornment: {
"& .MuiSvgIcon-root": {
fontSize: 20,
},
},
inputRoot: {
'&[class*="MuiOutlinedInput-root"]': {
"& $endAdornment": {
right: 6,
},
},
},
},
}
}
+57
View File
@@ -0,0 +1,57 @@
import { ComponentsProps } from "@material-ui/core/styles/props"
/**
* These are global overrides to MUI components and we may move away from using them in the future.
*/
export const props = {
MuiButton: {
variant: "contained",
},
MuiTextField: {
margin: "dense",
InputProps: {
labelWidth: 0,
},
variant: "outlined",
spellCheck: false,
},
MuiInputLabel: {
shrink: true,
},
MuiFormControl: {
variant: "outlined",
margin: "dense",
},
MuiInput: {
spellCheck: false,
autoCorrect: "off",
},
MuiOutlinedInput: {
notched: false,
},
MuiDialogTitle: {
disableTypography: true,
},
MuiMenu: {
anchorOrigin: {
vertical: "bottom",
horizontal: "right",
},
transformOrigin: {
vertical: "top",
horizontal: "right",
},
// Disable the behavior of placing the select at the selected option
getContentAnchorEl: null,
},
MuiList: {
disablePadding: true,
},
MuiTabs: {
textColor: "primary",
indicatorColor: "primary",
},
MuiTab: {
disableTouchRipple: true,
},
} as ComponentsProps
@@ -1,15 +1,23 @@
import { createMuiTheme } from "@material-ui/core/styles"
import { Overrides } from "@material-ui/core/styles/overrides"
import { borderRadius } from "./constants"
import { getOverrides } from "./overrides"
import { CustomPalette, darkPalette, lightPalette } from "./palettes"
import { props } from "./props"
import { typography } from "./typography"
const makeTheme = (palette: CustomPalette) => {
// Grab defaults to re-use in overrides
const { breakpoints } = createMuiTheme()
return createMuiTheme({
palette,
typography,
shape: {
borderRadius,
},
props,
overrides: getOverrides(palette, breakpoints) as Overrides,
})
}
+42
View File
@@ -0,0 +1,42 @@
export const appendCSSString = (cssString: string, cssClass: string): string => {
if (cssString === "") {
return cssClass
}
return `${cssString} ${cssClass}`
}
/**
* @param classes May be an object or an array. When using an object, each key
* is a css class name and its associated value indicates whether it should be
* applied. When using an array, only truthy values are applied.
* @returns A string with each class that should be included. Classes are
* separated by two spaces. If no classes are included, returns `undefined`.
* @example
* combineClasses() // -> undefined
* combineClasses({ text: true }) // -> "text"
* combineClasses({ text: true, success: true }) // -> "text success"
* combineClasses({ text: true, success: false }) // -> "text"
* combineClasses([ "text", false, "" ]) // -> "text"
*/
export const combineClasses = (
classes: Array<string | false | undefined> | Record<string, boolean | undefined> = {},
): string | undefined => {
let result = ""
if (Array.isArray(classes)) {
for (const cssClass of classes) {
if (cssClass) {
result = appendCSSString(result, cssClass)
}
}
} else {
for (const cssClass in classes) {
const useClass = classes[cssClass]
if (useClass) {
result = appendCSSString(result, cssClass)
}
}
}
return result.length ? result : undefined
}