mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Add sidebar and panel (#866)
* feat: Add sidebar and panel * fix: fix sidebar values
This commit is contained in:
@@ -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" },
|
||||
],
|
||||
}
|
||||
@@ -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" },
|
||||
],
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
Reference in New Issue
Block a user