improve sidebar

This commit is contained in:
srgooglo 2022-11-08 22:44:09 +00:00
parent e697bf43f3
commit 844ec95349
5 changed files with 325 additions and 386 deletions

View File

@ -1,59 +1,99 @@
import React from "react" import React from "react"
import { Layout, Menu, Avatar } from "antd" import { Menu, Avatar } from "antd"
import { Translation } from "react-i18next"
import classnames from "classnames" import classnames from "classnames"
import config from "config" import config from "config"
import { Icons, createIconRender } from "components/Icons" import { Icons, createIconRender } from "components/Icons"
import { sidebarKeys as defaultSidebarItems } from "schemas/defaultSettings" import { sidebarKeys as defaultSidebarItems } from "schemas/defaultSettings"
import sidebarItems from "schemas/routes.json" import sidebarItems from "schemas/routes.json"
import { Translation } from "react-i18next"
import { SidebarEditor } from "./components"
import "./index.less" import "./index.less"
const { Sider } = Layout
const onClickHandlers = { const onClickHandlers = {
settings: (event) => { settings: (event) => {
window.app.openSettings() window.app.openSettings()
}, },
} }
const getSidebarComponents = () => {
const items = {}
sidebarItems.forEach((item, index) => {
items[item.id] = {
...item,
index,
content: (
<>
{createIconRender(item.icon)} {item.title}
</>
),
}
})
return items
}
const generateItems = () => {
const components = getSidebarComponents()
const itemsMap = []
const pathResolvers = {}
const keys = window.app?.settings.get("sidebarKeys") ?? defaultSidebarItems
// filter undefined components to avoid error
keys.filter((key) => {
if (typeof components[key] !== "undefined") {
return true
}
})
keys.forEach((key, index) => {
const component = components[key]
try {
// avoid if item is duplicated
if (itemsMap.includes(component)) {
return false
}
if (typeof component.path !== "undefined") {
pathResolvers[component.id] = component.path
}
itemsMap.push(component)
} catch (error) {
return console.log(error)
}
})
return {
itemsMap,
pathResolvers,
}
}
export default class Sidebar extends React.Component { export default class Sidebar extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.controller = window.app["SidebarController"] = { this.controller = window.app["SidebarController"] = {
toggleVisibility: this.toggleVisibility, toggleVisibility: this.toggleVisibility,
toggleEdit: this.toggleEditMode,
toggleElevation: this.toggleElevation, toggleElevation: this.toggleElevation,
attachElement: this.attachElement, toggleCollapse: this.toggleCollapse,
isVisible: () => this.state.visible, isVisible: () => this.state.visible,
isEditMode: () => this.state.visible,
isCollapsed: () => this.state.collapsed, isCollapsed: () => this.state.collapsed,
} }
this.state = { this.state = {
editMode: false,
visible: false, visible: false,
loading: true,
collapsed: window.app.settings.get("collapseOnLooseFocus") ?? false,
pathResolve: {},
menus: {},
extraItems: {
bottom: [],
top: [],
},
elevated: false, elevated: false,
additionalElements: [], collapsed: window.app.settings.get("collapseOnLooseFocus") ?? false,
pathResolvers: null,
menus: null,
} }
window.app.eventBus.on("edit_sidebar", () => this.toggleEditMode())
window.app.eventBus.on("settingChanged.sidebar_collapse", (value) => {
this.toggleCollapse(value)
})
// handle sidedrawer open/close // handle sidedrawer open/close
window.app.eventBus.on("sidedrawer.hasDrawers", () => { window.app.eventBus.on("sidedrawer.hasDrawers", () => {
this.toggleElevation(true) this.toggleElevation(true)
@ -66,112 +106,21 @@ export default class Sidebar extends React.Component {
collapseDebounce = null collapseDebounce = null
componentDidMount = async () => { componentDidMount = async () => {
await this.loadSidebarItems() await this.loadItems()
setTimeout(() => { setTimeout(() => {
this.controller.toggleVisibility(true) this.controller.toggleVisibility(true)
}, 100) }, 100)
} }
getStoragedKeys = () => { loadItems = async () => {
return window.app.settings.get("sidebarKeys") const generation = generateItems()
}
attachElement = (element) => {
this.setState({
additionalElements: [...this.state.additionalElements, element],
})
}
appendItem = (item = {}) => {
const { position } = item
if (typeof position === "undefined" && typeof this.state.extraItems[position] === "undefined") {
console.error("Invalid position")
return false
}
const state = this.state.extraItems
state[position].push(item)
this.setState({ extraItems: state })
}
loadSidebarItems = () => {
const items = {}
const itemsMap = []
// parse all items from schema
sidebarItems.forEach((item, index) => {
items[item.id] = {
...item,
index,
content: (
<>
{createIconRender(item.icon)} {item.title}
</>
),
}
})
// filter undefined to avoid error
let keys = (this.getStoragedKeys() ?? defaultSidebarItems).filter((key) => {
if (typeof items[key] !== "undefined") {
return true
}
})
// short items
keys.forEach((id, index) => {
const item = items[id]
if (item.locked) {
if (item.index !== index) {
keys = keys.move(index, item.index)
//update index
window.app.settings.set("sidebarKeys", keys)
}
}
})
// set items from scoped keys
keys.forEach((key, index) => {
const item = items[key]
try {
// avoid if item is duplicated
if (itemsMap.includes(item)) {
return false
}
let valid = true
if (typeof item.requireState === "object") {
const { key, value } = item.requireState
//* TODO: check global state
}
// end validation
if (!valid) {
return false
}
if (typeof item.path !== "undefined") {
let resolvers = this.state.pathResolve ?? {}
resolvers[item.id] = item.path
this.setState({ pathResolve: resolvers })
}
itemsMap.push(item)
} catch (error) {
return console.log(error)
}
})
// update states // update states
this.setState({ items, menus: itemsMap, loading: false }) await this.setState({
menus: generation.itemsMap,
pathResolvers: generation.pathResolvers,
})
} }
renderMenuItems(items) { renderMenuItems(items) {
@ -184,29 +133,25 @@ export default class Sidebar extends React.Component {
return items.map((item) => { return items.map((item) => {
if (Array.isArray(item.children)) { if (Array.isArray(item.children)) {
return ( return <Menu.SubMenu
<Menu.SubMenu key={item.id}
key={item.id} icon={handleRenderIcon(item.icon)}
icon={handleRenderIcon(item.icon)} title={<span>
title={<span> <Translation>
<Translation> {t => t(item.title)}
{t => t(item.title)} </Translation>
</Translation> </span>}
</span>} {...item.props}
{...item.props} >
> {this.renderMenuItems(item.children)}
{this.renderMenuItems(item.children)} </Menu.SubMenu>
</Menu.SubMenu>
)
} }
return ( return <Menu.Item key={item.id} icon={handleRenderIcon(item.icon)} {...item.props}>
<Menu.Item key={item.id} icon={handleRenderIcon(item.icon)} {...item.props}> <Translation>
<Translation> {t => t(item.title ?? item.id)}
{t => t(item.title ?? item.id)} </Translation>
</Translation> </Menu.Item>
</Menu.Item>
)
}) })
} }
@ -223,27 +168,14 @@ export default class Sidebar extends React.Component {
if (typeof onClickHandlers[e.key] === "function") { if (typeof onClickHandlers[e.key] === "function") {
return onClickHandlers[e.key](e) return onClickHandlers[e.key](e)
} }
if (typeof this.state.pathResolve[e.key] !== "undefined") {
return window.app.setLocation(`/${this.state.pathResolve[e.key]}`, 150)
}
return window.app.setLocation(`/${e.key}`, 150) if (typeof this.state.pathResolvers === "object") {
} if (typeof this.state.pathResolvers[e.key] !== "undefined") {
return window.app.setLocation(`/${this.state.pathResolvers[e.key]}`, 150)
toggleEditMode = (to) => {
if (typeof to === "undefined") {
to = !this.state.editMode
}
if (to) {
window.app.eventBus.emit("clearAllOverlays")
} else {
if (this.itemsMap !== this.getStoragedKeys()) {
this.loadSidebarItems()
} }
} }
this.setState({ editMode: to, collapsed: false }) return window.app.setLocation(`/${e.key}`, 150)
} }
toggleCollapse = (to) => { toggleCollapse = (to) => {
@ -261,11 +193,12 @@ export default class Sidebar extends React.Component {
} }
onMouseEnter = () => { onMouseEnter = () => {
if (window.app.settings.is("collapseOnLooseFocus", false)) { if (!this.state.visible) return
return false
} if (window.app.settings.is("collapseOnLooseFocus", false)) return
clearTimeout(this.collapseDebounce) clearTimeout(this.collapseDebounce)
this.collapseDebounce = null this.collapseDebounce = null
if (this.state.collapsed) { if (this.state.collapsed) {
@ -274,49 +207,29 @@ export default class Sidebar extends React.Component {
} }
handleMouseLeave = () => { handleMouseLeave = () => {
if (window.app.settings.is("collapseOnLooseFocus", false)) { if (!this.state.visible) return
return false
} if (window.app.settings.is("collapseOnLooseFocus", false)) return
if (!this.state.collapsed) { if (!this.state.collapsed) {
this.collapseDebounce = setTimeout(() => { this.toggleCollapse(true) }, window.app.settings.get("autoCollapseDelay") ?? 500) this.collapseDebounce = setTimeout(() => { this.toggleCollapse(true) }, window.app.settings.get("autoCollapseDelay") ?? 500)
} }
} }
renderExtraItems = (position) => {
return this.state.extraItems[position].map((item = {}) => {
if (typeof item.icon !== "undefined") {
if (typeof item.props !== "object") {
item.props = Object()
}
item.props["icon"] = createIconRender(item.icon)
}
return <Menu.Item key={item.id} {...item.props}>{item.children}</Menu.Item>
})
}
render() { render() {
if (this.state.loading) return null if (!this.state.menus) return null
const { user } = this.props
return ( return (
<Sider <div
onMouseEnter={this.onMouseEnter} onMouseEnter={this.onMouseEnter}
onMouseLeave={this.handleMouseLeave} onMouseLeave={this.handleMouseLeave}
theme={this.props.theme}
width={this.state.editMode ? 400 : 200}
collapsed={this.state.editMode ? false : this.state.collapsed}
onCollapse={() => this.props.onCollapse()}
className={ className={
classnames( classnames(
"sidebar", "app_sidebar",
{ {
["edit_mode"]: this.state.editMode, ["collapsed"]: this.state.visible && this.state.collapsed,
["elevated"]: this.state.visible && this.state.elevated,
["hidden"]: !this.state.visible, ["hidden"]: !this.state.visible,
["elevated"]: this.state.elevated
} }
) )
} }
@ -327,70 +240,49 @@ export default class Sidebar extends React.Component {
</div> </div>
</div> </div>
{this.state.editMode && ( <div key="menu" className="app_sidebar_menu_wrapper">
<div style={{ height: "100%" }}> <Menu selectable={true} mode="inline" onClick={this.handleClick}>
<SidebarEditor /> {this.renderMenuItems(this.state.menus)}
</div> </Menu>
)} </div>
{!this.state.editMode && ( <div key="bottom" className={classnames("app_sidebar_menu_wrapper", "bottom")}>
<div key="menu" className="app_sidebar_menu"> <Menu selectable={false} mode="inline" onClick={this.handleClick}>
<Menu selectable={true} mode="inline" theme={this.props.theme} onClick={this.handleClick}> <Menu.Item key="search" icon={<Icons.Search />} overrideEvent="app.openSearcher" >
{this.renderMenuItems(this.state.menus)} <Translation>
{this.renderExtraItems("top")} {(t) => t("Search")}
</Menu> </Translation>
</div> </Menu.Item>
)} <Menu.Item key="create" icon={<Icons.PlusCircle />} overrideEvent="app.openCreator" >
<Translation>
{!this.state.editMode && <div key="additionalElements" className="additionalElements"> {(t) => t("Create")}
{this.state.additionalElements} </Translation>
</div>} </Menu.Item>
<Menu.Item key="notifications" icon={<Icons.Bell />} overrideEvent="app.openNotifications">
{!this.state.editMode && ( <Translation>
<div key="bottom" className="app_sidebar_bottom"> {t => t("Notifications")}
<Menu selectable={false} mode="inline" theme={this.props.theme} onClick={this.handleClick}> </Translation>
<Menu.Item key="search" icon={<Icons.Search />} overrideEvent="app.openSearcher" > </Menu.Item>
<Menu.Item key="settings" icon={<Icons.Settings />}>
<Translation>
{t => t("Settings")}
</Translation>
</Menu.Item>
{
app.userData && <Menu.Item key="account" className="user_avatar">
<Avatar shape="square" src={app.userData?.avatar} />
</Menu.Item>
}
{
!app.userData && <Menu.Item key="login" icon={<Icons.LogIn />}>
<Translation> <Translation>
{(t) => t("Search")} {t => t("Login")}
</Translation> </Translation>
</Menu.Item> </Menu.Item>
<Menu.Item key="create" icon={<Icons.PlusCircle />} overrideEvent="app.openCreator" > }
<Translation> </Menu>
{(t) => t("Create")} </div>
</Translation> </div>
</Menu.Item>
<Menu.Item key="notifications" icon={<Icons.Bell />} overrideEvent="app.openNotifications">
<Translation>
{t => t("Notifications")}
</Translation>
</Menu.Item>
<Menu.Item key="settings" icon={<Icons.Settings />}>
<Translation>
{t => t("Settings")}
</Translation>
</Menu.Item>
{
user && <Menu.Item key="account">
<div className="user_avatar">
<Avatar shape="square" src={user?.avatar} />
</div>
</Menu.Item>
}
{
!user && <Menu.Item key="login" icon={<Icons.LogIn />}>
<Translation>
{t => t("Login")}
</Translation>
</Menu.Item>
}
{this.renderExtraItems("bottom")}
</Menu>
</div>
)}
</Sider>
) )
} }
} }

View File

@ -1,18 +1,45 @@
@import "theme/vars.less"; @import "theme/vars.less";
// SIDEBAR .app_sidebar {
.ant-layout-sider { position: relative;
display: flex;
flex-direction: column;
left: 0;
top: 0;
z-index: 1000; z-index: 1000;
background: var(--sidebar-background-color) !important;
background-color: var(--sidebar-background-color) !important;
border-radius: 0 @app_sidebar_borderRadius @app_sidebar_borderRadius 0;
overflow: hidden; overflow: hidden;
border: 1px solid var(--sidebar-background-color);
width: @app_sidebar_collapsed_width;
min-width: @app_sidebar_width;
height: 100vh;
padding: 10px 0;
transition: all 150ms ease-in-out; transition: all 150ms ease-in-out;
background-color: var(--sidebar-background-color);
border-radius: 0 @app_sidebar_borderRadius @app_sidebar_borderRadius 0;
border: 1px solid var(--sidebar-background-color);
&.collapsed {
width: 80px;
min-width: 80px;
.app_sidebar_menu_wrapper {
.ant-menu {
.ant-menu-item:not(.user_avatar) {
.ant-menu-title-content {
animation: disappear 0.3s ease-out forwards;
}
}
}
}
}
&.hidden { &.hidden {
flex: 0 !important; flex: 0 !important;
min-width: 0 !important; min-width: 0 !important;
@ -23,138 +50,127 @@
&.elevated { &.elevated {
box-shadow: 0 0 5px 4px rgba(0, 0, 0, 0.1) !important; box-shadow: 0 0 5px 4px rgba(0, 0, 0, 0.1) !important;
} }
}
.ant-menu-item { .app_sidebar_header {
color: var(--background-color-contrast); display: flex;
flex-direction: column;
h1, height: 17%;
h2,
h3, padding: 10px 0;
h4,
h5, .app_sidebar_header_logo {
h6, display: flex;
span, align-items: center;
p { justify-content: center;
color: var(--background-color-contrast);
img {
user-select: none;
--webkit-user-select: none;
width: 80%;
max-height: 80px;
}
&.collapsed {
img {
max-width: 40px;
}
}
}
} }
}
.ant-menu, .app_sidebar_menu_wrapper {
.ant-menu ul { height: 65%;
background: transparent !important; width: 100%;
background-color: transparent !important;
border-right: 0 !important; overflow: overlay;
} overflow-x: hidden;
.sidebar .ant-layout-sider-children { display: flex;
margin-top: 15px !important; flex-direction: column;
margin-bottom: 15px !important;
background: transparent !important; align-items: center;
background-color: transparent !important;
user-select: none; transition: all 150ms ease-in-out;
--webkit-user-select: none;
transition: all 150ms ease-in-out; &.bottom {
height: 100%; position: absolute;
display: flex;
flex-direction: column;
&.edit_mode .ant-layout-sider-children { bottom: 0;
background: transparent !important; left: 0;
background-color: transparent !important;
margin-top: 15px !important; height: fit-content;
.app_sidebar_menu_wrapper { padding-bottom: 30px;
opacity: 0; }
height: 0;
overflow: hidden; .ant-menu {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
transition: all 150ms ease-in-out;
.ant-menu-item {
display: flex;
align-items: center;
justify-content: center;
width: 90%;
padding: 0 10px !important;
margin: 5px 0 !important;
transition: all 150ms ease-in-out;
.ant-menu-title-content {
flex: 1;
}
&.user_avatar {
.ant-menu-title-content {
display: inline-flex;
align-items: center;
justify-content: center;
width: fit-content;
}
margin: 0;
padding: 0;
}
svg {
width: fit-content;
margin: 0 !important;
height: 16px;
}
}
} }
} }
} }
.app_sidebar_menu_wrapper { @keyframes disappear {
transition: all 450ms ease-in-out; 0% {
opacity: 1;
height: 100%; width: 100%;
width: 100%;
}
.app_sidebar_header {
background: transparent !important;
background-color: transparent !important;
user-select: none;
--webkit-user-select: none;
display: flex;
flex-direction: column;
height: 15%;
margin-top: 5%;
margin-bottom: 5%;
}
.app_sidebar_header_logo {
user-select: none;
--webkit-user-select: none;
display: flex;
align-items: center;
justify-content: center;
img {
user-select: none;
--webkit-user-select: none;
width: 80%;
max-height: 80px;
} }
&.collapsed { 95% {
img { opacity: 0;
max-width: 40px; width: 0px;
} margin: 0;
} }
}
.app_sidebar_menu { 100% {
background: transparent !important; opacity: 0;
background-color: transparent !important; width: 0px;
margin: 0;
height: 65%; flex: 0;
overflow: overlay;
overflow-x: hidden;
}
.app_sidebar_bottom {
position: absolute;
bottom: 0;
padding-bottom: 30px;
z-index: 50;
left: 0;
background: transparent !important;
background-color: transparent !important;
backdrop-filter: blur(10px);
--webkit-backdrop-filter: blur(10px);
width: 100%;
height: fit-content;
.ant-menu,
ul {
background: transparent !important;
background-color: transparent !important;
} }
}
.user_avatar {
display: flex;
align-items: center;
justify-content: center;
padding: 0 !important;
} }

View File

@ -172,6 +172,29 @@
} }
// fix menu colors // fix menu colors
.ant-menu,
.ant-menu ul {
background: transparent !important;
background-color: transparent !important;
border-right: 0 !important;
.ant-menu-item {
color: var(--background-color-contrast);
h1,
h2,
h3,
h4,
h5,
h6,
span,
p {
color: var(--background-color-contrast);
}
}
}
.ant-menu-item { .ant-menu-item {
color: var(--text-color); color: var(--text-color);
border-radius: 8px; border-radius: 8px;
@ -391,6 +414,6 @@
// fix adm cards // fix adm cards
.adm-card { .adm-card {
background: var(--background-color-accent); background: var(--background-color-accent);
color: var(--text-color); color: var(--text-color);
} }

View File

@ -119,29 +119,25 @@ html {
} }
} }
.ant-layout,
.content_layout {
width: 100%;
height: 100%;
max-height: 100vh;
background-color: transparent;
}
.app_layout { .app_layout {
background-color: rgba(var(--layoutBackgroundColor), var(--backgroundColorTransparency)) !important;
backdrop-filter: blur(var(--backgroundBlur));
position: relative; position: relative;
-webkit-overflow-scrolling: touch;
width: 100%; width: 100%;
height: 100%; height: 100%;
max-height: 100vh; max-height: 100vh;
overflow: hidden; overflow: hidden;
display: flex;
flex-direction: row;
background-color: rgba(var(--layoutBackgroundColor), var(--backgroundColorTransparency)) !important;
backdrop-filter: blur(var(--backgroundBlur));
transition: all 150ms ease-in-out; transition: all 150ms ease-in-out;
-webkit-overflow-scrolling: touch;
::-webkit-scrollbar { ::-webkit-scrollbar {
display: block; display: block;
position: absolute; position: absolute;
@ -164,6 +160,15 @@ html {
} }
} }
.ant-layout,
.content_layout {
width: 100%;
height: 100%;
max-height: 100vh;
background-color: transparent;
}
.layout_page { .layout_page {
position: relative; position: relative;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;

View File

@ -1,5 +1,8 @@
//* Now this only works as an fallback for unset dynamic theme values //* Now this only works as an fallback for unset dynamic theme values
@app_sidebar_collapsed_width: 80px;
@app_sidebar_width: 230px;
// borders & radius // borders & radius
@app_sidebar_borderRadius: 18px; @app_sidebar_borderRadius: 18px;