init: add fuse-react v8.3.5 skeleton

This commit is contained in:
2023-05-30 21:07:44 +01:00
commit 3a760d2646
543 changed files with 102541 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
const fuseDark = {
50: '#e5e6e8',
100: '#bec1c5',
200: '#92979f',
300: '#666d78',
400: '#464e5b',
500: '#252f3e',
600: '#212a38',
700: '#1b2330',
800: '#161d28',
900: '#0d121b',
A100: '#5d8eff',
A200: '#2a6aff',
A400: '#004af6',
A700: '#0042dd',
contrastDefaultColor: 'light',
};
export default fuseDark;

View File

@@ -0,0 +1,2 @@
export { default as fuseDark } from './fuseDark';
export { default as skyBlue } from './skyBlue';

View File

@@ -0,0 +1,19 @@
const skyBlue = {
50: '#e4fafd',
100: '#bdf2fa',
200: '#91e9f7',
300: '#64e0f3',
400: '#43daf1',
500: '#22d3ee',
600: '#1eceec',
700: '#19c8e9',
800: '#14c2e7',
900: '#0cb7e2',
A100: '#ffffff',
A200: '#daf7ff',
A400: '#a7ecff',
A700: '#8de6ff',
contrastDefaultColor: 'dark',
};
export default skyBlue;

View File

@@ -0,0 +1,25 @@
import { useLayoutEffect, useState } from 'react';
import history from '@history';
import { Router } from 'react-router-dom';
function BrowserRouter({ basename, children, window }) {
const [state, setState] = useState({
action: history.action,
location: history.location,
});
useLayoutEffect(() => history.listen(setState), [history]);
return (
<Router
basename={basename}
location={state.location}
navigationType={state.action}
navigator={history}
>
{children}
</Router>
);
}
export default BrowserRouter;

View File

@@ -0,0 +1 @@
export { default } from './BrowserRouter';

View File

@@ -0,0 +1,121 @@
import { memo } from 'react';
function DemoContent() {
return (
<div>
<img
src="assets/images/demo-content/morain-lake.jpg"
alt="beach"
style={{
maxWidth: '640px',
width: '100%',
}}
className="rounded-6"
/>
<h1 className="py-16 font-semibold">Early Sunrise</h1>
<h4 className="pb-12 font-medium">Demo Content</h4>
<p>
One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in
his bed into a horrible vermin. He lay on his armour-like back, and if he lifted his head a
little he could see his brown belly, slightly domed and divided by arches into stiff
sections.
</p>
<blockquote>
<p>
The bedding was hardly able to cover it and seemed ready to slide off any moment. His many
legs, pitifully thin compared with the size of the rest of him, waved about helplessly as
he looked. "What's happened to me? " he thought. It wasn't a dream.
</p>
<footer>John Doe</footer>
</blockquote>
<p>
His room, a proper human room although a little too small, lay peacefully between its four
familiar walls. A collection of textile samples lay spread out on the table - Samsa was a
travelling salesman - and above it there hung a picture that he had recently cut out of an
illustrated magazine and housed in a nice, gilded frame.
</p>
<p>
It showed a lady fitted out with a fur hat and fur boa who sat upright, raising a heavy fur
muff that covered the whole of her lower arm towards the viewer. Gregor then turned to look
out the window at the dull weather. Drops of rain could be heard hitting the pane, which
made him feel quite sad.
</p>
<p>
"How about if I sleep a little bit longer and forget all this nonsense", he thought, but
that was something he was unable to do because he was used to sleeping on his right, and in
his present state couldn't get into that position. However hard he threw himself onto his
right, he always rolled back to where he was.
</p>
<p>
He must have tried it a hundred times, shut his eyes so that he wouldn't have to look at the
floundering legs, and only stopped when he began to feel a mild, dull pain there that he had
never felt before. "Oh, God", he thought, "what a strenuous career it is that I've chosen!
</p>
<p>
Travelling day in and day out. Doing business like this takes much more effort than doing
your own business at home, and on top of that there's the curse of travelling, worries about
making train connections, bad and irregular food, contact with different people all the time
so that you can never get to know anyone or become friendly with them.
</p>
<p>
"He felt a slight itch up on his belly; pushed himself slowly up on his back towards the
headboard so that he could lift his head better; found where the itch was, and saw that it
was covered with lots of little white spots which he didn't know what to make of; and when
he tried to feel the place with one of his legs he drew it quickly back because as soon as
he touched it he was overcome by a cold shudder. He slid back into his former position.
</p>
<p>
"Getting up early all the time", he thought, "it makes you stupid. You've got to get enough
sleep. Other travelling salesmen live a life of luxury. For instance, whenever I go back to
the guest house during the morning to copy out the contract, these gentlemen are always
still sitting there eating their breakfasts. I ought to just try that with my boss; I'd get
kicked out on the spot. But who knows, maybe that would be the best thing for me...
</p>
<p>
His room, a proper human room although a little too small, lay peacefully between its four
familiar walls. A collection of textile samples lay spread out on the table - Samsa was a
travelling salesman - and above it there hung a picture that he had recently cut out of an
illustrated magazine and housed in a nice, gilded frame.
</p>
<p>
It showed a lady fitted out with a fur hat and fur boa who sat upright, raising a heavy fur
muff that covered the whole of her lower arm towards the viewer. Gregor then turned to look
out the window at the dull weather. Drops of rain could be heard hitting the pane, which
made him feel quite sad.
</p>
<p>
"How about if I sleep a little bit longer and forget all this nonsense", he thought, but
that was something he was unable to do because he was used to sleeping on his right, and in
his present state couldn't get into that position. However hard he threw himself onto his
right, he always rolled back to where he was.
</p>
<p>
He must have tried it a hundred times, shut his eyes so that he wouldn't have to look at the
floundering legs, and only stopped when he began to feel a mild, dull pain there that he had
never felt before. "Oh, God", he thought, "what a strenuous career it is that I've chosen!
</p>
<p>
Travelling day in and day out. Doing business like this takes much more effort than doing
your own business at home, and on top of that there's the curse of travelling, worries about
making train connections, bad and irregular food, contact with different people all the time
so that you can never get to know anyone or become friendly with them.
</p>
<p>
"He felt a slight itch up on his belly; pushed himself slowly up on his back towards the
headboard so that he could lift his head better; found where the itch was, and saw that it
was covered with lots of little white spots which he didn't know what to make of; and when
he tried to feel the place with one of his legs he drew it quickly back because as soon as
he touched it he was overcome by a cold shudder. He slid back into his former position.
</p>
<p>
"Getting up early all the time", he thought, "it makes you stupid. You've got to get enough
sleep. Other travelling salesmen live a life of luxury. For instance, whenever I go back to
the guest house during the morning to copy out the contract, these gentlemen are always
still sitting there eating their breakfasts. I ought to just try that with my boss; I'd get
kicked out on the spot. But who knows, maybe that would be the best thing for me...
</p>
</div>
);
}
export default memo(DemoContent);

View File

@@ -0,0 +1 @@
export { default } from './DemoContent';

View File

@@ -0,0 +1,29 @@
import _ from '@lodash';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import { cloneElement, memo } from 'react';
function DemoSidebarContent() {
function generate(element) {
return _(30).times((value) =>
cloneElement(element, {
key: value,
})
);
}
return (
<div>
<List dense>
{generate(
<ListItem button>
<ListItemText primary="Single-line item" />
</ListItem>
)}
</List>
</div>
);
}
export default memo(DemoSidebarContent);

View File

@@ -0,0 +1 @@
export { default } from './DemoSidebarContent';

View File

@@ -0,0 +1,80 @@
import FuseUtils from '@fuse/utils';
import AppContext from 'app/AppContext';
import { Component } from 'react';
import { matchRoutes } from 'react-router-dom';
import withRouter from '@fuse/core/withRouter';
import history from '@history';
let loginRedirectUrl = null;
class FuseAuthorization extends Component {
constructor(props, context) {
super(props);
const { routes } = context;
this.state = {
accessGranted: true,
routes,
};
this.defaultLoginRedirectUrl = props.loginRedirectUrl || '/';
}
componentDidMount() {
if (!this.state.accessGranted) {
this.redirectRoute();
}
}
shouldComponentUpdate(nextProps, nextState) {
return nextState.accessGranted !== this.state.accessGranted;
}
componentDidUpdate() {
if (!this.state.accessGranted) {
this.redirectRoute();
}
}
static getDerivedStateFromProps(props, state) {
const { location, userRole } = props;
const { pathname } = location;
const matchedRoutes = matchRoutes(state.routes, pathname);
const matched = matchedRoutes ? matchedRoutes[0] : false;
return {
accessGranted: matched ? FuseUtils.hasPermission(matched.route.auth, userRole) : true,
};
}
redirectRoute() {
const { location, userRole } = this.props;
const { pathname } = location;
const redirectUrl = loginRedirectUrl || this.defaultLoginRedirectUrl;
/*
User is guest
Redirect to Login Page
*/
if (!userRole || userRole.length === 0) {
setTimeout(() => history.push('/sign-in'), 0);
loginRedirectUrl = pathname;
} else {
/*
User is member
User must be on unAuthorized page or just logged in
Redirect to dashboard or loginRedirectUrl
*/
setTimeout(() => history.push(redirectUrl), 0);
loginRedirectUrl = this.defaultLoginRedirectUrl;
}
}
render() {
// console.info('Fuse Authorization rendered', this.state.accessGranted);
return this.state.accessGranted ? <>{this.props.children}</> : null;
}
}
FuseAuthorization.contextType = AppContext;
export default withRouter(FuseAuthorization);

View File

@@ -0,0 +1 @@
export { default } from './FuseAuthorization';

View File

@@ -0,0 +1,97 @@
import Typography from '@mui/material/Typography';
import clsx from 'clsx';
import moment from 'moment';
import PropTypes from 'prop-types';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
function FuseCountdown(props) {
const { onComplete } = props;
const [endDate] = useState(
moment.isMoment(props.endDate) ? props.endDate : moment(props.endDate)
);
const [countdown, setCountdown] = useState({
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
});
const intervalRef = useRef();
const complete = useCallback(() => {
window.clearInterval(intervalRef.current);
if (onComplete) {
onComplete();
}
}, [onComplete]);
const tick = useCallback(() => {
const currDate = moment();
const diff = endDate.diff(currDate, 'seconds');
if (diff < 0) {
complete();
return;
}
const timeLeft = moment.duration(diff, 'seconds');
setCountdown({
days: timeLeft.asDays().toFixed(0),
hours: timeLeft.hours(),
minutes: timeLeft.minutes(),
seconds: timeLeft.seconds(),
});
}, [complete, endDate]);
useEffect(() => {
intervalRef.current = setInterval(tick, 1000);
return () => {
clearInterval(intervalRef.current);
};
}, [tick]);
return (
<div className={clsx('flex items-center', props.className)}>
<div className="flex flex-col items-center justify-center px-12">
<Typography variant="h4" className="mb-4">
{countdown.days}
</Typography>
<Typography variant="caption" color="text.secondary">
days
</Typography>
</div>
<div className="flex flex-col items-center justify-center px-12">
<Typography variant="h4" className="mb-4">
{countdown.hours}
</Typography>
<Typography variant="caption" color="text.secondary">
hours
</Typography>
</div>
<div className="flex flex-col items-center justify-center px-12">
<Typography variant="h4" className="mb-4">
{countdown.minutes}
</Typography>
<Typography variant="caption" color="text.secondary">
minutes
</Typography>
</div>
<div className="flex flex-col items-center justify-center px-12">
<Typography variant="h4" className="mb-4">
{countdown.seconds}
</Typography>
<Typography variant="caption" color="text.secondary">
seconds
</Typography>
</div>
</div>
);
}
FuseCountdown.propTypes = {
endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
onComplete: PropTypes.func,
};
FuseCountdown.defaultProps = {
endDate: moment().add(15, 'days'),
};
export default memo(FuseCountdown);

View File

@@ -0,0 +1 @@
export { default } from './FuseCountdown';

View File

@@ -0,0 +1,27 @@
import Dialog from '@mui/material/Dialog';
import { useDispatch, useSelector } from 'react-redux';
import {
closeDialog,
selectFuseDialogOptions,
selectFuseDialogState,
} from 'app/store/fuse/dialogSlice';
function FuseDialog(props) {
const dispatch = useDispatch();
const state = useSelector(selectFuseDialogState);
const options = useSelector(selectFuseDialogOptions);
return (
<Dialog
open={state}
onClose={(ev) => dispatch(closeDialog())}
aria-labelledby="fuse-dialog-title"
classes={{
paper: 'rounded-8',
}}
{...options}
/>
);
}
export default FuseDialog;

View File

@@ -0,0 +1 @@
export { default } from './FuseDialog';

View File

@@ -0,0 +1,110 @@
import * as React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import rtlPlugin from 'stylis-plugin-rtl';
import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import { StyleSheetManager } from 'styled-components';
import { styled, useTheme } from '@mui/material/styles';
import GlobalStyles from '@mui/material/GlobalStyles';
function FramedDemo(props) {
const { children, document } = props;
const theme = useTheme();
React.useEffect(() => {
document.body.dir = theme.direction;
}, [document, theme.direction]);
const cache = React.useMemo(
() =>
createCache({
key: `iframe-demo-${theme.direction}`,
prepend: true,
container: document.head,
stylisPlugins: theme.direction === 'rtl' ? [rtlPlugin] : [],
}),
[document, theme.direction]
);
const getWindow = React.useCallback(() => document.defaultView, [document]);
return (
<StyleSheetManager
target={document.head}
stylisPlugins={theme.direction === 'rtl' ? [rtlPlugin] : []}
>
<CacheProvider value={cache}>
<GlobalStyles
styles={() => ({
html: {
fontSize: '62.5%',
},
})}
/>
{React.cloneElement(children, {
window: getWindow,
})}
</CacheProvider>
</StyleSheetManager>
);
}
FramedDemo.propTypes = {
children: PropTypes.node,
document: PropTypes.object.isRequired,
};
const Frame = styled('iframe')(({ theme }) => ({
backgroundColor: theme.palette.background.default,
flexGrow: 1,
height: 400,
border: 0,
boxShadow: theme.shadows[1],
}));
function DemoFrame(props) {
const { children, name, ...other } = props;
const title = `${name} demo`;
/**
* @type {import('react').Ref<HTMLIFrameElement>}
*/
const frameRef = React.useRef(null);
// If we portal content into the iframe before the load event then that content
// is dropped in firefox.
const [iframeLoaded, onLoad] = React.useReducer(() => true, false);
React.useEffect(() => {
const document = frameRef.current.contentDocument;
// When we hydrate the iframe then the load event is already dispatched
// once the iframe markup is parsed (maybe later but the important part is
// that it happens before React can attach event listeners).
// We need to check the readyState of the document once the iframe is mounted
// and "replay" the missed load event.
// See https://github.com/facebook/react/pull/13862 for ongoing effort in React
// (though not with iframes in mind).
if (document != null && document.readyState === 'complete' && !iframeLoaded) {
onLoad();
}
}, [iframeLoaded]);
const document = frameRef.current?.contentDocument;
return (
<>
<Frame onLoad={onLoad} ref={frameRef} title={title} {...other} />
{iframeLoaded !== false
? ReactDOM.createPortal(
<FramedDemo document={document}>{children}</FramedDemo>,
document.body
)
: null}
</>
);
}
DemoFrame.propTypes = {
children: PropTypes.node.isRequired,
name: PropTypes.string.isRequired,
};
export default React.memo(DemoFrame);

View File

@@ -0,0 +1,106 @@
import FuseHighlight from '@fuse/core/FuseHighlight';
import Card from '@mui/material/Card';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { useState, useMemo } from 'react';
import { darken } from '@mui/material/styles';
import Box from '@mui/material/Box';
import DemoFrame from './DemoFrame';
import FuseSvgIcon from '../FuseSvgIcon';
const propTypes = {
name: PropTypes.string,
raw: PropTypes.object,
currentTabIndex: PropTypes.number,
};
const defaultProps = {
name: '',
currentTabIndex: 0,
};
function FuseExample(props) {
const [currentTab, setCurrentTab] = useState(props.currentTabIndex);
const { component: Component, raw, iframe, className, name } = props;
function handleChange(event, value) {
setCurrentTab(value);
}
return (
<Card
className={clsx(className, 'shadow')}
sx={{
backgroundColor: (theme) =>
darken(theme.palette.background.paper, theme.palette.mode === 'light' ? 0.01 : 0.1),
}}
>
<Box
sx={{
backgroundColor: (theme) =>
darken(theme.palette.background.paper, theme.palette.mode === 'light' ? 0.02 : 0.2),
}}
>
<Tabs
classes={{
root: 'border-b-1',
flexContainer: 'justify-end',
}}
value={currentTab}
onChange={handleChange}
textColor="secondary"
indicatorColor="secondary"
>
{Component && (
<Tab
classes={{ root: 'min-w-64' }}
icon={<FuseSvgIcon>heroicons-outline:eye</FuseSvgIcon>}
/>
)}
{raw && (
<Tab
classes={{ root: 'min-w-64' }}
icon={<FuseSvgIcon>heroicons-outline:code</FuseSvgIcon>}
/>
)}
</Tabs>
</Box>
<div className="flex justify-center max-w-full relative">
<div className={currentTab === 0 ? 'flex flex-1 max-w-full' : 'hidden'}>
{Component &&
(iframe ? (
<DemoFrame name={name}>
<Component />
</DemoFrame>
) : (
<div className="p-24 flex flex-1 justify-center max-w-full">
<Component />
</div>
))}
</div>
<div className={currentTab === 1 ? 'flex flex-1' : 'hidden'}>
{useMemo(() => {
return raw && currentTab === 1 ? (
<div className="flex flex-1">
<FuseHighlight
component="pre"
className="language-javascript w-full"
sx={{ borderRadius: '0!important' }}
>
{raw.default}
</FuseHighlight>
</div>
) : null;
}, [raw, currentTab])}
</div>
</div>
</Card>
);
}
FuseExample.propTypes = propTypes;
FuseExample.defaultProps = defaultProps;
export default FuseExample;

View File

@@ -0,0 +1 @@
export { default } from './FuseExample';

View File

@@ -0,0 +1,82 @@
import * as Prism from 'prismjs';
import PropTypes from 'prop-types';
import { useEffect, useMemo, useRef } from 'react';
import './prism-languages';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
function FuseHighlight(props) {
const { async, children, className, component: Wrapper } = props;
const domNode = useRef(null);
useEffect(() => {
if (domNode.current) {
Prism.highlightElement(domNode.current, async);
}
}, [children, async]);
return useMemo(() => {
const trimCode = () => {
let sourceString = children;
if (typeof sourceString === 'object' && sourceString.default) {
sourceString = sourceString.default;
}
// Split the source into lines
const sourceLines = sourceString.split('\n');
// Remove the first and the last line of the source
// code if they are blank lines. This way, the html
// can be formatted properly while using fuse-highlight
// component
if (!sourceLines[0].trim()) {
sourceLines.shift();
}
if (!sourceLines[sourceLines.length - 1].trim()) {
sourceLines.pop();
}
// Find the first non-whitespace char index in
// the first line of the source code
const indexOfFirstChar = sourceLines[0].search(/\S|$/);
// Generate the trimmed source
let sourceRaw = '';
// Iterate through all the lines
sourceLines.forEach((line, index) => {
// Trim the beginning white space depending on the index
// and concat the source code
sourceRaw += line.substr(indexOfFirstChar, line.length);
// If it's not the last line...
if (index !== sourceLines.length - 1) {
// Add a line break at the end
sourceRaw = `${sourceRaw}\n`;
}
});
return sourceRaw || '';
};
return (
<>
<Wrapper ref={domNode} className={clsx('border', className)}>
{/* {trimCode()} */}
{trimCode()}
</Wrapper>
</>
);
}, [children, className]);
}
FuseHighlight.propTypes = {
component: PropTypes.node,
};
FuseHighlight.defaultProps = {
component: 'code',
};
export default styled(FuseHighlight)``;

View File

@@ -0,0 +1 @@
export { default } from './FuseHighlight';

View File

@@ -0,0 +1,18 @@
import 'prismjs/components/prism-c';
import 'prismjs/components/prism-cpp';
import 'prismjs/components/prism-csharp';
import 'prismjs/components/prism-css';
import 'prismjs/components/prism-diff';
import 'prismjs/components/prism-java';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-json';
import 'prismjs/components/prism-jsx';
import 'prismjs/components/prism-markup';
import 'prismjs/components/prism-markup-templating';
import 'prismjs/components/prism-perl';
import 'prismjs/components/prism-php';
import 'prismjs/components/prism-python';
import 'prismjs/components/prism-sass';
import 'prismjs/components/prism-scss';
import 'prismjs/components/prism-typescript';
import 'prismjs/prism';

View File

@@ -0,0 +1,148 @@
import { useDeepCompareEffect } from '@fuse/hooks';
import _ from '@lodash';
import AppContext from 'app/AppContext';
import {
generateSettings,
selectFuseCurrentSettings,
selectFuseDefaultSettings,
setSettings,
} from 'app/store/fuse/settingsSlice';
import { memo, useCallback, useContext, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { matchRoutes, useLocation } from 'react-router-dom';
import GlobalStyles from '@mui/material/GlobalStyles';
import { alpha } from '@mui/material/styles';
const inputGlobalStyles = (
<GlobalStyles
styles={(theme) => ({
html: {
backgroundColor: `${theme.palette.background.default}!important`,
color: `${theme.palette.text.primary}!important`,
},
body: {
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
},
/* 'code:not([class*="language-"])': {
color: theme.palette.secondary.dark,
backgroundColor:
theme.palette.mode === 'light' ? 'rgba(255, 255, 255, .9)' : 'rgba(0, 0, 0, .9)',
padding: '2px 3px',
borderRadius: 2,
lineHeight: 1.7,
}, */
'table.simple tbody tr th': {
borderColor: theme.palette.divider,
},
'table.simple thead tr th': {
borderColor: theme.palette.divider,
},
'a:not([role=button]):not(.MuiButtonBase-root)': {
color: theme.palette.secondary.main,
textDecoration: 'underline',
'&:hover': {},
},
'a.link, a:not([role=button])[target=_blank]': {
background: alpha(theme.palette.secondary.main, 0.2),
color: 'inherit',
borderBottom: `1px solid ${theme.palette.divider}`,
textDecoration: 'none',
'&:hover': {
background: alpha(theme.palette.secondary.main, 0.3),
textDecoration: 'none',
},
},
'[class^="border"]': {
borderColor: theme.palette.divider,
},
'[class*="border"]': {
borderColor: theme.palette.divider,
},
'[class*="divide-"] > :not([hidden]) ~ :not([hidden])': {
borderColor: theme.palette.divider,
},
hr: {
borderColor: theme.palette.divider,
},
'::-webkit-scrollbar-thumb': {
boxShadow: `inset 0 0 0 20px ${
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.24)' : 'rgba(255, 255, 255, 0.24)'
}`,
},
'::-webkit-scrollbar-thumb:active': {
boxShadow: `inset 0 0 0 20px ${
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.37)' : 'rgba(255, 255, 255, 0.37)'
}`,
},
})}
/>
);
function FuseLayout(props) {
const { layouts } = props;
const dispatch = useDispatch();
const settings = useSelector(selectFuseCurrentSettings);
const defaultSettings = useSelector(selectFuseDefaultSettings);
const appContext = useContext(AppContext);
const { routes } = appContext;
const location = useLocation();
const { pathname } = location;
const matchedRoutes = matchRoutes(routes, pathname);
const matched = matchedRoutes ? matchedRoutes[0] : false;
const newSettings = useRef(null);
const shouldAwaitRender = useCallback(() => {
let _newSettings;
/**
* On Path changed
*/
// if (prevPathname !== pathname) {
if (matched && matched.route.settings) {
/**
* if matched route has settings
*/
const routeSettings = matched.route.settings;
_newSettings = generateSettings(defaultSettings, routeSettings);
} else if (!_.isEqual(newSettings.current, defaultSettings)) {
/**
* Reset to default settings on the new path
*/
_newSettings = _.merge({}, defaultSettings);
} else {
_newSettings = newSettings.current;
}
if (!_.isEqual(newSettings.current, _newSettings)) {
newSettings.current = _newSettings;
}
}, [defaultSettings, matched]);
shouldAwaitRender();
useDeepCompareEffect(() => {
if (!_.isEqual(newSettings.current, settings)) {
dispatch(setSettings(newSettings.current));
}
}, [dispatch, newSettings.current, settings]);
// console.warn('::FuseLayout:: rendered');
const Layout = useMemo(() => layouts[settings.layout.style], [layouts, settings.layout.style]);
return _.isEqual(newSettings.current, settings) ? (
<>
{inputGlobalStyles}
<Layout {...props} />
</>
) : null;
}
export default memo(FuseLayout);

View File

@@ -0,0 +1 @@
export { default } from './FuseLayout';

View File

@@ -0,0 +1,49 @@
import { useTimeout } from '@fuse/hooks';
import Typography from '@mui/material/Typography';
import PropTypes from 'prop-types';
import { useState } from 'react';
import clsx from 'clsx';
import Box from '@mui/material/Box';
function FuseLoading(props) {
const [showLoading, setShowLoading] = useState(!props.delay);
useTimeout(() => {
setShowLoading(true);
}, props.delay);
return (
<div
className={clsx(
'flex flex-1 flex-col items-center justify-center p-24',
!showLoading && 'hidden'
)}
>
<Typography className="text-13 sm:text-20 font-medium -mb-16" color="text.secondary">
Loading
</Typography>
<Box
id="spinner"
sx={{
'& > div': {
backgroundColor: 'palette.secondary.main',
},
}}
>
<div className="bounce1" />
<div className="bounce2" />
<div className="bounce3" />
</Box>
</div>
);
}
FuseLoading.propTypes = {
delay: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
};
FuseLoading.defaultProps = {
delay: false,
};
export default FuseLoading;

View File

@@ -0,0 +1 @@
export { default } from './FuseLoading';

View File

@@ -0,0 +1,91 @@
import { amber, blue, green } from '@mui/material/colors';
import { styled } from '@mui/material/styles';
import IconButton from '@mui/material/IconButton';
import Snackbar from '@mui/material/Snackbar';
import SnackbarContent from '@mui/material/SnackbarContent';
import Typography from '@mui/material/Typography';
import { memo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
hideMessage,
selectFuseMessageOptions,
selectFuseMessageState,
} from 'app/store/fuse/messageSlice';
import FuseSvgIcon from '../FuseSvgIcon';
const StyledSnackbar = styled(Snackbar)(({ theme, variant }) => ({
'& .FuseMessage-content': {
...(variant === 'success' && {
backgroundColor: green[600],
color: '#FFFFFF',
}),
...(variant === 'error' && {
backgroundColor: theme.palette.error.dark,
color: theme.palette.getContrastText(theme.palette.error.dark),
}),
...(variant === 'info' && {
backgroundColor: blue[600],
color: '#FFFFFF',
}),
...(variant === 'warning' && {
backgroundColor: amber[600],
color: '#FFFFFF',
}),
},
}));
const variantIcon = {
success: 'check_circle',
warning: 'warning',
error: 'error_outline',
info: 'info',
};
function FuseMessage(props) {
const dispatch = useDispatch();
const state = useSelector(selectFuseMessageState);
const options = useSelector(selectFuseMessageOptions);
return (
<StyledSnackbar
{...options}
open={state}
onClose={() => dispatch(hideMessage())}
ContentProps={{
variant: 'body2',
headlineMapping: {
body1: 'div',
body2: 'div',
},
}}
>
<SnackbarContent
className="FuseMessage-content"
message={
<div className="flex items-center">
{variantIcon[options.variant] && (
<FuseSvgIcon color="inherit">{variantIcon[options.variant]}</FuseSvgIcon>
)}
<Typography className="mx-8">{options.message}</Typography>
</div>
}
action={[
<IconButton
key="close"
aria-label="Close"
color="inherit"
onClick={() => dispatch(hideMessage())}
size="large"
>
<FuseSvgIcon>heroicons-outline:x</FuseSvgIcon>
</IconButton>,
]}
/>
</StyledSnackbar>
);
}
export default memo(FuseMessage);

View File

@@ -0,0 +1 @@
export { default } from './FuseMessage';

View File

@@ -0,0 +1,44 @@
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { memo } from 'react';
const Root = styled('div')(({ theme }) => ({
padding: '0 7px',
fontSize: 11,
fontWeight: 600,
height: 20,
minWidth: 20,
borderRadius: 20,
display: 'flex',
alignItems: 'center',
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
}));
function FuseNavBadge(props) {
const { className, badge } = props;
return (
<Root
className={clsx('item-badge', className, badge?.classes)}
style={{
backgroundColor: badge.bg,
color: badge.fg,
}}
>
{badge.title}
</Root>
);
}
FuseNavBadge.propTypes = {
badge: PropTypes.shape({
title: PropTypes.node,
bg: PropTypes.string,
fg: PropTypes.string,
}),
};
FuseNavBadge.defaultProps = {};
export default memo(FuseNavBadge);

View File

@@ -0,0 +1,10 @@
const components = {};
export function registerComponent(name, Component) {
components[name] = Component;
}
export default function FuseNavItem(props) {
const C = components[props.type];
return C ? <C {...props} /> : null;
}

View File

@@ -0,0 +1,91 @@
import Divider from '@mui/material/Divider';
import PropTypes from 'prop-types';
import { memo } from 'react';
import _ from '@lodash';
import GlobalStyles from '@mui/material/GlobalStyles';
import FuseNavHorizontalLayout1 from './horizontal/FuseNavHorizontalLayout1';
import FuseNavVerticalLayout1 from './vertical/FuseNavVerticalLayout1';
import FuseNavVerticalLayout2 from './vertical/FuseNavVerticalLayout2';
import FuseNavHorizontalCollapse from './horizontal/types/FuseNavHorizontalCollapse';
import FuseNavHorizontalGroup from './horizontal/types/FuseNavHorizontalGroup';
import FuseNavHorizontalItem from './horizontal/types/FuseNavHorizontalItem';
import FuseNavHorizontalLink from './horizontal/types/FuseNavHorizontalLink';
import FuseNavVerticalCollapse from './vertical/types/FuseNavVerticalCollapse';
import FuseNavVerticalGroup from './vertical/types/FuseNavVerticalGroup';
import FuseNavVerticalItem from './vertical/types/FuseNavVerticalItem';
import FuseNavVerticalLink from './vertical/types/FuseNavVerticalLink';
import { registerComponent } from './FuseNavItem';
const inputGlobalStyles = (
<GlobalStyles
styles={(theme) => ({
'.popper-navigation-list': {
'& .fuse-list-item': {
padding: '8px 12px 8px 12px',
height: 40,
minHeight: 40,
'& .fuse-list-item-text': {
padding: '0 0 0 8px',
},
},
'&.dense': {
'& .fuse-list-item': {
minHeight: 32,
height: 32,
'& .fuse-list-item-text': {
padding: '0 0 0 8px',
},
},
},
},
})}
/>
);
/*
Register Fuse Navigation Components
*/
registerComponent('vertical-group', FuseNavVerticalGroup);
registerComponent('vertical-collapse', FuseNavVerticalCollapse);
registerComponent('vertical-item', FuseNavVerticalItem);
registerComponent('vertical-link', FuseNavVerticalLink);
registerComponent('horizontal-group', FuseNavHorizontalGroup);
registerComponent('horizontal-collapse', FuseNavHorizontalCollapse);
registerComponent('horizontal-item', FuseNavHorizontalItem);
registerComponent('horizontal-link', FuseNavHorizontalLink);
registerComponent('vertical-divider', () => <Divider className="my-16" />);
registerComponent('horizontal-divider', () => <Divider className="my-16" />);
function FuseNavigation(props) {
const options = _.pick(props, [
'navigation',
'layout',
'active',
'dense',
'className',
'onItemClick',
'firstLevel',
'selectedId',
]);
if (props.navigation.length > 0) {
return (
<>
{inputGlobalStyles}
{props.layout === 'horizontal' && <FuseNavHorizontalLayout1 {...options} />}
{props.layout === 'vertical' && <FuseNavVerticalLayout1 {...options} />}
{props.layout === 'vertical-2' && <FuseNavVerticalLayout2 {...options} />}
</>
);
}
return null;
}
FuseNavigation.propTypes = {
navigation: PropTypes.array.isRequired,
};
FuseNavigation.defaultProps = {
layout: 'vertical',
};
export default memo(FuseNavigation);

View File

@@ -0,0 +1,59 @@
import List from '@mui/material/List';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import FuseNavItem from '../FuseNavItem';
const StyledList = styled(List)(({ theme }) => ({
'& .fuse-list-item': {
'&:hover': {
backgroundColor:
theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0,0,0,.04)',
},
'&:focus:not(.active)': {
backgroundColor:
theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.06)' : 'rgba(0,0,0,.05)',
},
padding: '8px 12px 8px 12px',
height: 40,
minHeight: 40,
'&.level-0': {
minHeight: 44,
minminHeight: 44,
},
'& .fuse-list-item-text': {
padding: '0 0 0 8px',
},
},
'&.active-square-list': {
'& .fuse-list-item': {
borderRadius: '0',
},
},
}));
function FuseNavHorizontalLayout1(props) {
const { navigation, layout, active, dense, className } = props;
return (
<StyledList
className={clsx(
'navigation whitespace-nowrap flex p-0',
`active-${active}-list`,
dense && 'dense',
className
)}
>
{navigation.map((_item) => (
<FuseNavItem
key={_item.id}
type={`horizontal-${_item.type}`}
item={_item}
nestedLevel={0}
dense={dense}
/>
))}
</StyledList>
);
}
export default FuseNavHorizontalLayout1;

View File

@@ -0,0 +1,190 @@
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
import { styled, useTheme } from '@mui/material/styles';
import { useDebounce } from '@fuse/hooks';
import Grow from '@mui/material/Grow';
import IconButton from '@mui/material/IconButton';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Paper from '@mui/material/Paper';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { memo, useMemo, useState } from 'react';
import * as ReactDOM from 'react-dom';
import { Manager, Popper, Reference } from 'react-popper';
import withRouter from '@fuse/core/withRouter';
import FuseNavBadge from '../../FuseNavBadge';
import FuseNavItem from '../../FuseNavItem';
import FuseSvgIcon from '../../../FuseSvgIcon';
const StyledListItem = styled(ListItem)(({ theme }) => ({
color: theme.palette.text.primary,
minHeight: 48,
'&.active, &.active:hover, &.active:focus': {
backgroundColor: `${theme.palette.secondary.main}!important`,
color: `${theme.palette.secondary.contrastText}!important`,
'&.open': {
backgroundColor: 'rgba(0,0,0,.08)',
},
'& > .fuse-list-item-text': {
padding: '0 0 0 16px',
},
'& .fuse-list-item-icon': {
color: 'inherit',
},
},
}));
function isUrlInChildren(parent, url) {
if (!parent.children) {
return false;
}
for (let i = 0; i < parent.children.length; i += 1) {
if (parent.children[i].children) {
if (isUrlInChildren(parent.children[i], url)) {
return true;
}
}
if (parent.children[i].url === url || url.includes(parent.children[i].url)) {
return true;
}
}
return false;
}
function FuseNavHorizontalCollapse(props) {
const [opened, setOpened] = useState(false);
const { item, nestedLevel, dense } = props;
const theme = useTheme();
const handleToggle = useDebounce((open) => {
setOpened(open);
}, 150);
return useMemo(
() => (
<ul className="relative px-0">
<Manager>
<Reference>
{({ ref }) => (
<div ref={ref}>
<StyledListItem
button
className={clsx(
'fuse-list-item',
opened && 'open',
isUrlInChildren(item, props.location.pathname) && 'active'
)}
onMouseEnter={() => handleToggle(true)}
onMouseLeave={() => handleToggle(false)}
aria-owns={opened ? 'menu-fuse-list-grow' : null}
aria-haspopup="true"
component={item.url ? NavLinkAdapter : 'li'}
to={item.url}
end={item.end}
role="button"
sx={item.sx}
disabled={item.disabled}
>
{item.icon && (
<FuseSvgIcon
color="action"
className={clsx('fuse-list-item-icon shrink-0', item.iconClass)}
>
{item.icon}
</FuseSvgIcon>
)}
<ListItemText
className="fuse-list-item-text"
primary={item.title}
classes={{ primary: 'text-13 truncate' }}
/>
{item.badge && <FuseNavBadge className="mx-4" badge={item.badge} />}
<IconButton
disableRipple
className="w-16 h-16 ltr:ml-4 rtl:mr-4 p-0"
color="inherit"
size="large"
>
<FuseSvgIcon size={16} className="arrow-icon">
{theme.direction === 'ltr'
? 'heroicons-outline:arrow-sm-right'
: 'heroicons-outline:arrow-sm-left'}
</FuseSvgIcon>
</IconButton>
</StyledListItem>
</div>
)}
</Reference>
{ReactDOM.createPortal(
<Popper
placement={theme.direction === 'ltr' ? 'right' : 'left'}
eventsEnabled={opened}
positionFixed
>
{({ ref, style, placement, arrowProps }) =>
opened && (
<div
ref={ref}
style={{
...style,
zIndex: 999 + nestedLevel + 1,
}}
data-placement={placement}
className={clsx('z-999', !opened && 'pointer-events-none')}
>
<Grow in={opened} id="menu-fuse-list-grow" style={{ transformOrigin: '0 0 0' }}>
<Paper
className="rounded-8"
onMouseEnter={() => handleToggle(true)}
onMouseLeave={() => handleToggle(false)}
>
{item.children && (
<ul className={clsx('popper-navigation-list', dense && 'dense', 'px-0')}>
{item.children.map((_item) => (
<FuseNavItem
key={_item.id}
type={`horizontal-${_item.type}`}
item={_item}
nestedLevel={nestedLevel + 1}
dense={dense}
/>
))}
</ul>
)}
</Paper>
</Grow>
</div>
)
}
</Popper>,
document.querySelector('#root')
)}
</Manager>
</ul>
),
[dense, handleToggle, item, nestedLevel, opened, props.location.pathname, theme.direction]
);
}
FuseNavHorizontalCollapse.propTypes = {
item: PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string,
icon: PropTypes.string,
children: PropTypes.array,
}),
};
FuseNavHorizontalCollapse.defaultProps = {};
const NavHorizontalCollapse = withRouter(memo(FuseNavHorizontalCollapse));
export default NavHorizontalCollapse;

View File

@@ -0,0 +1,194 @@
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
import { styled, useTheme } from '@mui/material/styles';
import { useDebounce } from '@fuse/hooks';
import Grow from '@mui/material/Grow';
import IconButton from '@mui/material/IconButton';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Paper from '@mui/material/Paper';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { memo, useMemo, useState } from 'react';
import * as ReactDOM from 'react-dom';
import { Manager, Popper, Reference } from 'react-popper';
import withRouter from '@fuse/core/withRouter';
import FuseNavItem from '../../FuseNavItem';
import FuseSvgIcon from '../../../FuseSvgIcon';
const StyledListItem = styled(ListItem)(({ theme }) => ({
color: theme.palette.text.primary,
'&.active, &.active:hover, &.active:focus': {
backgroundColor: `${theme.palette.secondary.main}!important`,
color: `${theme.palette.secondary.contrastText}!important`,
'& .fuse-list-item-text-primary': {
color: 'inherit',
},
'& .fuse-list-item-icon': {
color: 'inherit',
},
},
'& .fuse-list-item-text': {
padding: '0 0 0 16px',
},
'&.level-0': {
minHeight: 44,
borderRadius: 4,
'&:hover': {
background: 'transparent',
},
},
}));
function isUrlInChildren(parent, url) {
if (!parent.children) {
return false;
}
for (let i = 0; i < parent.children.length; i += 1) {
if (parent.children[i].children) {
if (isUrlInChildren(parent.children[i], url)) {
return true;
}
}
if (parent.children[i].url === url || url.includes(parent.children[i].url)) {
return true;
}
}
return false;
}
function FuseNavHorizontalGroup(props) {
const [opened, setOpened] = useState(false);
const { item, nestedLevel, dense } = props;
const theme = useTheme();
const handleToggle = useDebounce((open) => {
setOpened(open);
}, 150);
return useMemo(() => {
let popperPlacement = 'left';
if (nestedLevel === 0) {
popperPlacement = theme.direction === 'ltr' ? 'bottom-start' : 'bottom-end';
} else {
popperPlacement = theme.direction === 'ltr' ? 'right' : 'left';
}
return (
<Manager>
<Reference>
{({ ref }) => (
<div ref={ref}>
<StyledListItem
button
className={clsx(
'fuse-list-item',
'relative',
`level-${nestedLevel}`,
isUrlInChildren(item, props.location.pathname) && 'active'
)}
onMouseEnter={() => handleToggle(true)}
onMouseLeave={() => handleToggle(false)}
aria-owns={opened ? 'menu-fuse-list-grow' : null}
aria-haspopup="true"
component={item.url ? NavLinkAdapter : 'li'}
to={item.url}
end={item.end}
role="button"
sx={item.sx}
disabled={item.disabled}
>
{item.icon && (
<FuseSvgIcon
color="action"
className={clsx('fuse-list-item-icon shrink-0', item.iconClass)}
>
{item.icon}
</FuseSvgIcon>
)}
<ListItemText
className="fuse-list-item-text"
primary={item.title}
classes={{ primary: 'text-13 truncate' }}
/>
{nestedLevel > 0 && (
<IconButton
disableRipple
className="w-16 h-16 ltr:ml-4 rtl:mr-4 p-0"
color="inherit"
size="large"
>
<FuseSvgIcon size={16} className="arrow-icon">
{theme.direction === 'ltr'
? 'heroicons-outline:arrow-sm-right'
: 'heroicons-outline:arrow-sm-left'}
</FuseSvgIcon>
</IconButton>
)}
</StyledListItem>
</div>
)}
</Reference>
{ReactDOM.createPortal(
<Popper placement={popperPlacement} eventsEnabled={opened} positionFixed>
{({ ref, style, placement, arrowProps }) =>
opened && (
<div
ref={ref}
style={{
...style,
zIndex: 999 + nestedLevel,
}}
data-placement={placement}
className={clsx('z-999', !opened && 'pointer-events-none')}
>
<Grow in={opened} id="menu-fuse-list-grow" style={{ transformOrigin: '0 0 0' }}>
<Paper
className="rounded-8"
onMouseEnter={() => handleToggle(true)}
onMouseLeave={() => handleToggle(false)}
>
{item.children && (
<ul className={clsx('popper-navigation-list', dense && 'dense', 'px-0')}>
{item.children.map((_item) => (
<FuseNavItem
key={_item.id}
type={`horizontal-${_item.type}`}
item={_item}
nestedLevel={nestedLevel}
dense={dense}
/>
))}
</ul>
)}
</Paper>
</Grow>
</div>
)
}
</Popper>,
document.querySelector('#root')
)}
</Manager>
);
}, [dense, handleToggle, item, nestedLevel, opened, props.location.pathname, theme.direction]);
}
FuseNavHorizontalGroup.propTypes = {
item: PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string,
children: PropTypes.array,
}),
};
FuseNavHorizontalGroup.defaultProps = {};
const NavHorizontalGroup = withRouter(memo(FuseNavHorizontalGroup));
export default NavHorizontalGroup;

View File

@@ -0,0 +1,84 @@
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
import { styled } from '@mui/material/styles';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { memo, useMemo } from 'react';
import withRouter from '@fuse/core/withRouter';
import FuseNavBadge from '../../FuseNavBadge';
import FuseSvgIcon from '../../../FuseSvgIcon';
const StyledListItem = styled(ListItem)(({ theme }) => ({
color: theme.palette.text.primary,
textDecoration: 'none!important',
minHeight: 48,
'&.active': {
backgroundColor: `${theme.palette.secondary.main}!important`,
color: `${theme.palette.secondary.contrastText}!important`,
pointerEvents: 'none',
'& .fuse-list-item-text-primary': {
color: 'inherit',
},
'& .fuse-list-item-icon': {
color: 'inherit',
},
},
'& .fuse-list-item-icon': {},
'& .fuse-list-item-text': {
padding: '0 0 0 16px',
},
}));
function FuseNavHorizontalItem(props) {
const { item } = props;
return useMemo(
() => (
<StyledListItem
button
component={NavLinkAdapter}
to={item.url || ''}
activeClassName={item.url ? 'active' : ''}
className={clsx('fuse-list-item', item.active && 'active')}
end={item.end}
role="button"
sx={item.sx}
disabled={item.disabled}
>
{item.icon && (
<FuseSvgIcon
className={clsx('fuse-list-item-icon shrink-0', item.iconClass)}
color="action"
>
{item.icon}
</FuseSvgIcon>
)}
<ListItemText
className="fuse-list-item-text"
primary={item.title}
classes={{ primary: 'text-13 fuse-list-item-text-primary truncate' }}
/>
{item.badge && <FuseNavBadge className="ltr:ml-8 rtl:mr-8" badge={item.badge} />}
</StyledListItem>
),
[item.badge, item.exact, item.icon, item.iconClass, item.title, item.url]
);
}
FuseNavHorizontalItem.propTypes = {
item: PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string,
icon: PropTypes.string,
url: PropTypes.string,
}),
};
FuseNavHorizontalItem.defaultProps = {};
const NavHorizontalItem = withRouter(memo(FuseNavHorizontalItem));
export default NavHorizontalItem;

View File

@@ -0,0 +1,83 @@
import { styled } from '@mui/material/styles';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { memo, useMemo } from 'react';
import withRouter from '@fuse/core/withRouter';
import FuseNavBadge from '../../FuseNavBadge';
import FuseSvgIcon from '../../../FuseSvgIcon';
const StyledListItem = styled(ListItem)(({ theme }) => ({
color: theme.palette.text.primary,
textDecoration: 'none!important',
minHeight: 48,
'&.active': {
backgroundColor: `${theme.palette.secondary.main}!important`,
color: `${theme.palette.secondary.contrastText}!important`,
pointerEvents: 'none',
'& .fuse-list-item-text-primary': {
color: 'inherit',
},
'& .fuse-list-item-icon': {
color: 'inherit',
},
},
'& .fuse-list-item-icon': {},
'& .fuse-list-item-text': {
padding: '0 0 0 16px',
},
}));
function FuseNavHorizontalLink(props) {
const { item } = props;
return useMemo(
() => (
<StyledListItem
button
component="a"
href={item.url}
target={item.target ? item.target : '_blank'}
className={clsx('fuse-list-item')}
role="button"
sx={item.sx}
disabled={item.disabled}
>
{item.icon && (
<FuseSvgIcon
className={clsx('fuse-list-item-icon shrink-0', item.iconClass)}
color="action"
>
{item.icon}
</FuseSvgIcon>
)}
<ListItemText
className="fuse-list-item-text"
primary={item.title}
classes={{ primary: 'text-13 fuse-list-item-text-primary truncate' }}
/>
{item.badge && <FuseNavBadge className="ltr:ml-8 rtl:mr-8" badge={item.badge} />}
</StyledListItem>
),
[item.badge, item.icon, item.iconClass, item.target, item.title, item.url]
);
}
FuseNavHorizontalLink.propTypes = {
item: PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string,
icon: PropTypes.string,
url: PropTypes.string,
target: PropTypes.string,
}),
};
FuseNavHorizontalLink.defaultProps = {};
const NavHorizontalLink = withRouter(memo(FuseNavHorizontalLink));
export default NavHorizontalLink;

View File

@@ -0,0 +1 @@
export { default } from './FuseNavigation';

View File

@@ -0,0 +1,69 @@
import List from '@mui/material/List';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import { useDispatch } from 'react-redux';
import FuseNavItem from '../FuseNavItem';
const StyledList = styled(List)(({ theme }) => ({
'& .fuse-list-item': {
'&:hover': {
backgroundColor:
theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0,0,0,.04)',
},
'&:focus:not(.active)': {
backgroundColor:
theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.06)' : 'rgba(0,0,0,.05)',
},
},
'& .fuse-list-item-text': {
margin: 0,
},
'& .fuse-list-item-text-primary': {
lineHeight: '20px',
},
'&.active-square-list': {
'& .fuse-list-item, & .active.fuse-list-item': {
width: '100%',
borderRadius: '0',
},
},
'&.dense': {
'& .fuse-list-item': {
paddingTop: 0,
paddingBottom: 0,
height: 32,
},
},
}));
function FuseNavVerticalLayout1(props) {
const { navigation, layout, active, dense, className, onItemClick } = props;
const dispatch = useDispatch();
function handleItemClick(item) {
onItemClick?.(item);
}
return (
<StyledList
className={clsx(
'navigation whitespace-nowrap px-12 py-0',
`active-${active}-list`,
dense && 'dense',
className
)}
>
{navigation.map((_item) => (
<FuseNavItem
key={_item.id}
type={`vertical-${_item.type}`}
item={_item}
nestedLevel={0}
onItemClick={handleItemClick}
/>
))}
</StyledList>
);
}
export default FuseNavVerticalLayout1;

View File

@@ -0,0 +1,63 @@
import List from '@mui/material/List';
import { styled, useTheme } from '@mui/material/styles';
import clsx from 'clsx';
import FuseNavVerticalTab from './types/FuseNavVerticalTab';
const StyledList = styled(List)(({ theme }) => ({
'& .fuse-list-item': {
'&:hover': {
backgroundColor:
theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0,0,0,.04)',
},
'&:focus:not(.active)': {
backgroundColor:
theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.06)' : 'rgba(0,0,0,.05)',
},
},
'& .fuse-list-item-text-primary': {
lineHeight: '1',
},
'&.active-square-list': {
'& .fuse-list-item, & .active.fuse-list-item': {
width: '100%',
borderRadius: '0',
},
},
'&.dense': {},
}));
function FuseNavVerticalLayout2(props) {
const { navigation, layout, active, dense, className, onItemClick, firstLevel, selectedId } =
props;
const theme = useTheme();
function handleItemClick(item) {
onItemClick?.(item);
}
return (
<StyledList
className={clsx(
'navigation whitespace-nowrap items-center flex flex-col',
`active-${active}-list`,
dense && 'dense',
className
)}
>
{navigation.map((_item) => (
<FuseNavVerticalTab
key={_item.id}
type={`vertical-${_item.type}`}
item={_item}
nestedLevel={0}
onItemClick={handleItemClick}
firstLevel={firstLevel}
dense={dense}
selectedId={selectedId}
/>
))}
</StyledList>
);
}
export default FuseNavVerticalLayout2;

View File

@@ -0,0 +1,168 @@
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
import { alpha, styled } from '@mui/material/styles';
import Collapse from '@mui/material/Collapse';
import IconButton from '@mui/material/IconButton';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import List from '@mui/material/List';
import FuseNavBadge from '../../FuseNavBadge';
import FuseNavItem from '../../FuseNavItem';
import FuseSvgIcon from '../../../FuseSvgIcon';
const Root = styled(List)(({ theme, ...props }) => ({
padding: 0,
'&.open': {},
'& > .fuse-list-item': {
minHeight: 44,
width: '100%',
borderRadius: '6px',
margin: '0 0 4px 0',
paddingRight: 16,
paddingLeft: props.itempadding > 80 ? 80 : props.itempadding,
paddingTop: 10,
paddingBottom: 10,
color: alpha(theme.palette.text.primary, 0.7),
'&:hover': {
color: theme.palette.text.primary,
},
'& > .fuse-list-item-icon': {
marginRight: 16,
color: 'inherit',
},
},
}));
function needsToBeOpened(location, item) {
return location && isUrlInChildren(item, location.pathname);
}
function isUrlInChildren(parent, url) {
if (!parent.children) {
return false;
}
for (let i = 0; i < parent.children.length; i += 1) {
if (parent.children[i].children) {
if (isUrlInChildren(parent.children[i], url)) {
return true;
}
}
if (parent.children[i].url === url || url.includes(parent.children[i].url)) {
return true;
}
}
return false;
}
function FuseNavVerticalCollapse(props) {
const [open, setOpen] = useState(() => needsToBeOpened(props.location, props.item));
const { item, nestedLevel, onItemClick } = props;
const itempadding = nestedLevel > 0 ? 38 + nestedLevel * 16 : 16;
const location = useLocation();
useEffect(() => {
if (needsToBeOpened(location, props.item)) {
if (!open) {
setOpen(true);
}
}
// eslint-disable-next-line
}, [location, props.item]);
return useMemo(
() => (
<Root className={clsx(open && 'open')} itempadding={itempadding} sx={item.sx}>
<ListItem
component={item.url ? NavLinkAdapter : 'li'}
button
className="fuse-list-item"
onClick={() => setOpen(!open)}
to={item.url}
end={item.end}
role="button"
disabled={item.disabled}
>
{item.icon && (
<FuseSvgIcon
className={clsx('fuse-list-item-icon shrink-0', item.iconClass)}
color="action"
>
{item.icon}
</FuseSvgIcon>
)}
<ListItemText
className="fuse-list-item-text"
primary={item.title}
secondary={item.subtitle}
classes={{
primary: 'text-13 font-medium fuse-list-item-text-primary truncate',
secondary:
'text-11 font-medium fuse-list-item-text-secondary leading-normal truncate',
}}
/>
{item.badge && <FuseNavBadge className="mx-4" badge={item.badge} />}
<IconButton
disableRipple
className="w-20 h-20 -mx-12 p-0 focus:bg-transparent hover:bg-transparent"
onClick={(ev) => ev.preventDefault()}
size="large"
>
<FuseSvgIcon size={16} className="arrow-icon" color="inherit">
{open ? 'heroicons-solid:chevron-down' : 'heroicons-solid:chevron-right'}
</FuseSvgIcon>
</IconButton>
</ListItem>
{item.children && (
<Collapse in={open} className="collapse-children">
{item.children.map((_item) => (
<FuseNavItem
key={_item.id}
type={`vertical-${_item.type}`}
item={_item}
nestedLevel={nestedLevel + 1}
onItemClick={onItemClick}
/>
))}
</Collapse>
)}
</Root>
),
[
item.badge,
item.children,
item.icon,
item.iconClass,
item.title,
item.url,
itempadding,
nestedLevel,
onItemClick,
open,
]
);
}
FuseNavVerticalCollapse.propTypes = {
item: PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string,
icon: PropTypes.string,
children: PropTypes.array,
}),
};
FuseNavVerticalCollapse.defaultProps = {};
const NavVerticalCollapse = FuseNavVerticalCollapse;
export default NavVerticalCollapse;

View File

@@ -0,0 +1,102 @@
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
import { alpha, styled } from '@mui/material/styles';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { useMemo } from 'react';
import ListItem from '@mui/material/ListItem';
import { ListItemText } from '@mui/material';
import FuseNavItem from '../../FuseNavItem';
const Root = styled(ListItem)(({ theme, itempadding, ...props }) => ({
minminHeight: 44,
width: '100%',
borderRadius: '6px',
margin: '28px 0 0 0',
paddingRight: 16,
paddingLeft: props.itempadding > 80 ? 80 : props.itempadding,
paddingTop: 10,
paddingBottom: 10,
color: alpha(theme.palette.text.primary, 0.7),
fontWeight: 600,
letterSpacing: '0.025em',
}));
function FuseNavVerticalGroup(props) {
const { item, nestedLevel, onItemClick } = props;
const itempadding = nestedLevel > 0 ? 38 + nestedLevel * 16 : 16;
return useMemo(
() => (
<>
<Root
component={item.url ? NavLinkAdapter : 'li'}
itempadding={itempadding}
className={clsx(
'fuse-list-subheader flex items-center py-10',
!item.url && 'cursor-default'
)}
onClick={() => onItemClick && onItemClick(item)}
to={item.url}
end={item.end}
role="button"
sx={item.sx}
disabled={item.disabled}
>
<ListItemText
className="fuse-list-subheader-text"
sx={{
margin: 0,
'& > .MuiListItemText-primary': {
fontSize: 12,
color: 'secondary.light',
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '.05em',
lineHeight: '20px',
},
'& > .MuiListItemText-secondary': {
fontSize: 11,
color: 'text.disabled',
letterSpacing: '.06px',
fontWeight: 500,
lineHeight: '1.5',
},
}}
primary={item.title}
secondary={item.subtitle}
/>
</Root>
{item.children && (
<>
{item.children.map((_item) => (
<FuseNavItem
key={_item.id}
type={`vertical-${_item.type}`}
item={_item}
nestedLevel={nestedLevel}
onItemClick={onItemClick}
/>
))}
</>
)}
</>
),
[item, itempadding, nestedLevel, onItemClick]
);
}
FuseNavVerticalGroup.propTypes = {
item: PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string,
children: PropTypes.array,
}),
};
FuseNavVerticalGroup.defaultProps = {};
const NavVerticalGroup = FuseNavVerticalGroup;
export default NavVerticalGroup;

View File

@@ -0,0 +1,106 @@
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
import { alpha, styled } from '@mui/material/styles';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { useMemo } from 'react';
import FuseNavBadge from '../../FuseNavBadge';
import FuseSvgIcon from '../../../FuseSvgIcon';
const Root = styled(ListItem)(({ theme, ...props }) => ({
minHeight: 44,
width: '100%',
borderRadius: '6px',
margin: '0 0 4px 0',
paddingRight: 16,
paddingLeft: props.itempadding > 80 ? 80 : props.itempadding,
paddingTop: 10,
paddingBottom: 10,
color: alpha(theme.palette.text.primary, 0.7),
cursor: 'pointer',
textDecoration: 'none!important',
'&:hover': {
color: theme.palette.text.primary,
},
'&.active': {
color: theme.palette.text.primary,
backgroundColor:
theme.palette.mode === 'light'
? 'rgba(0, 0, 0, .05)!important'
: 'rgba(255, 255, 255, .1)!important',
pointerEvents: 'none',
transition: 'border-radius .15s cubic-bezier(0.4,0.0,0.2,1)',
'& > .fuse-list-item-text-primary': {
color: 'inherit',
},
'& > .fuse-list-item-icon': {
color: 'inherit',
},
},
'& >.fuse-list-item-icon': {
marginRight: 16,
color: 'inherit',
},
'& > .fuse-list-item-text': {},
}));
function FuseNavVerticalItem(props) {
const { item, nestedLevel, onItemClick } = props;
const itempadding = nestedLevel > 0 ? 38 + nestedLevel * 16 : 16;
return useMemo(
() => (
<Root
button
component={NavLinkAdapter}
to={item.url || ''}
activeClassName={item.url ? 'active' : ''}
className={clsx('fuse-list-item', item.active && 'active')}
onClick={() => onItemClick && onItemClick(item)}
end={item.end}
itempadding={itempadding}
role="button"
sx={item.sx}
disabled={item.disabled}
>
{item.icon && (
<FuseSvgIcon
className={clsx('fuse-list-item-icon shrink-0', item.iconClass)}
color="action"
>
{item.icon}
</FuseSvgIcon>
)}
<ListItemText
className="fuse-list-item-text"
primary={item.title}
secondary={item.subtitle}
classes={{
primary: 'text-13 font-medium fuse-list-item-text-primary truncate',
secondary: 'text-11 font-medium fuse-list-item-text-secondary leading-normal truncate',
}}
/>
{item.badge && <FuseNavBadge badge={item.badge} />}
</Root>
),
[item, itempadding, onItemClick]
);
}
FuseNavVerticalItem.propTypes = {
item: PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string,
icon: PropTypes.string,
url: PropTypes.string,
}),
};
FuseNavVerticalItem.defaultProps = {};
const NavVerticalItem = FuseNavVerticalItem;
export default NavVerticalItem;

View File

@@ -0,0 +1,99 @@
import { styled } from '@mui/material/styles';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { useMemo } from 'react';
import { useDispatch } from 'react-redux';
import FuseNavBadge from '../../FuseNavBadge';
import FuseSvgIcon from '../../../FuseSvgIcon';
const Root = styled(ListItem)(({ theme, ...props }) => ({
minHeight: 44,
width: '100%',
borderRadius: '6px',
margin: '0 0 4px 0',
paddingRight: 16,
paddingLeft: props.itempadding > 80 ? 80 : props.itempadding,
paddingTop: 10,
paddingBottom: 10,
'&.active': {
backgroundColor: `${theme.palette.secondary.main}!important`,
color: `${theme.palette.secondary.contrastText}!important`,
pointerEvents: 'none',
transition: 'border-radius .15s cubic-bezier(0.4,0.0,0.2,1)',
'& > .fuse-list-item-text-primary': {
color: 'inherit',
},
'& > .fuse-list-item-icon': {
color: 'inherit',
},
},
'& > .fuse-list-item-icon': {
marginRight: 16,
},
'& > .fuse-list-item-text': {},
color: theme.palette.text.primary,
textDecoration: 'none!important',
}));
function FuseNavVerticalLink(props) {
const dispatch = useDispatch();
const { item, nestedLevel, onItemClick } = props;
const itempadding = nestedLevel > 0 ? 38 + nestedLevel * 16 : 16;
return useMemo(
() => (
<Root
button
component="a"
href={item.url}
target={item.target ? item.target : '_blank'}
className="fuse-list-item"
onClick={() => onItemClick && onItemClick(item)}
role="button"
itempadding={itempadding}
sx={item.sx}
disabled={item.disabled}
>
{item.icon && (
<FuseSvgIcon
className={clsx('fuse-list-item-icon shrink-0', item.iconClass)}
color="action"
>
{item.icon}
</FuseSvgIcon>
)}
<ListItemText
className="fuse-list-item-text"
primary={item.title}
secondary={item.subtitle}
classes={{
primary: 'text-13 font-medium fuse-list-item-text-primary truncate',
secondary: 'text-11 font-medium fuse-list-item-text-secondary leading-normal truncate',
}}
/>
{item.badge && <FuseNavBadge badge={item.badge} />}
</Root>
),
[item, itempadding, onItemClick]
);
}
FuseNavVerticalLink.propTypes = {
item: PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string,
icon: PropTypes.string,
url: PropTypes.string,
target: PropTypes.string,
}),
};
FuseNavVerticalLink.defaultProps = {};
const NavVerticalLink = FuseNavVerticalLink;
export default NavVerticalLink;

View File

@@ -0,0 +1,177 @@
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
import { alpha, styled } from '@mui/material/styles';
import Tooltip from '@mui/material/Tooltip';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { Box } from '@mui/system';
import FuseNavBadge from '../../FuseNavBadge';
import FuseSvgIcon from '../../../FuseSvgIcon';
const Root = styled(Box)(({ theme }) => ({
'& > .fuse-list-item': {
minHeight: 100,
height: 100,
width: 100,
borderRadius: 12,
margin: '0 0 4px 0',
color: alpha(theme.palette.text.primary, 0.7),
cursor: 'pointer',
textDecoration: 'none!important',
padding: 0,
'&.dense': {
minHeight: 52,
height: 52,
width: 52,
},
'&.type-divider': {
padding: 0,
height: 2,
minHeight: 2,
margin: '12px 0',
backgroundColor:
theme.palette.mode === 'light'
? 'rgba(0, 0, 0, .05)!important'
: 'rgba(255, 255, 255, .1)!important',
pointerEvents: 'none',
},
'&:hover': {
color: theme.palette.text.primary,
},
'&.active': {
color: theme.palette.text.primary,
backgroundColor:
theme.palette.mode === 'light'
? 'rgba(0, 0, 0, .05)!important'
: 'rgba(255, 255, 255, .1)!important',
// pointerEvents: 'none',
transition: 'border-radius .15s cubic-bezier(0.4,0.0,0.2,1)',
'& .fuse-list-item-text-primary': {
color: 'inherit',
},
'& .fuse-list-item-icon': {
color: 'inherit',
},
},
'& .fuse-list-item-icon': {
color: 'inherit',
},
'& .fuse-list-item-text': {},
},
}));
function FuseNavVerticalTab(props) {
const dispatch = useDispatch();
const location = useLocation();
const { item, onItemClick, firstLevel, dense, selectedId } = props;
return useMemo(
() => (
<Root sx={item.sx}>
<ListItem
button
component={item.url && NavLinkAdapter}
to={item.url}
end={item.end}
className={clsx(
`type-${item.type}`,
dense && 'dense',
selectedId === item.id && 'active',
'fuse-list-item flex flex-col items-center justify-center p-12'
)}
onClick={() => onItemClick && onItemClick(item)}
role="button"
disabled={item.disabled}
>
{dense ? (
<Tooltip title={item.title || ''} placement="right">
<div className="w-32 h-32 min-h-32 flex items-center justify-center relative">
{item.icon ? (
<FuseSvgIcon
className={clsx('fuse-list-item-icon', item.iconClass)}
color="action"
>
{item.icon}
</FuseSvgIcon>
) : (
item.title && <div className="font-bold text-16">{item.title[0]}</div>
)}
{item.badge && (
<FuseNavBadge
badge={item.badge}
className="absolute top-0 ltr:right-0 rtl:left-0 min-w-16 h-16 p-4 justify-center"
/>
)}
</div>
</Tooltip>
) : (
<>
<div className="w-32 h-32 min-h-32 flex items-center justify-center relative mb-8">
{item.icon ? (
<FuseSvgIcon
size={32}
className={clsx('fuse-list-item-icon', item.iconClass)}
color="action"
>
{item.icon}
</FuseSvgIcon>
) : (
item.title && <div className="font-bold text-20">{item.title[0]}</div>
)}
{item.badge && (
<FuseNavBadge
badge={item.badge}
className="absolute top-0 ltr:right-0 rtl:left-0 min-w-16 h-16 p-4 justify-center"
/>
)}
</div>
<ListItemText
className="fuse-list-item-text grow-0 w-full"
primary={item.title}
classes={{
primary:
'text-12 font-medium fuse-list-item-text-primary truncate text-center truncate',
}}
/>
</>
)}
</ListItem>
{!firstLevel &&
item.children &&
item.children.map((_item) => (
<NavVerticalTab
key={_item.id}
type={`vertical-${_item.type}`}
item={_item}
nestedLevel={0}
onItemClick={onItemClick}
dense={dense}
selectedId={selectedId}
/>
))}
</Root>
),
[firstLevel, item, onItemClick, dense, selectedId]
);
}
FuseNavVerticalTab.propTypes = {
item: PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string,
icon: PropTypes.string,
url: PropTypes.string,
}),
};
FuseNavVerticalTab.defaultProps = {};
const NavVerticalTab = FuseNavVerticalTab;
export default NavVerticalTab;

View File

@@ -0,0 +1,281 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import * as PropTypes from 'prop-types';
import { forwardRef, memo, useImperativeHandle, useRef } from 'react';
import GlobalStyles from '@mui/material/GlobalStyles';
import FusePageCardedHeader from './FusePageCardedHeader';
import FusePageCardedSidebar from './FusePageCardedSidebar';
const Root = styled('div')(({ theme, ...props }) => ({
display: 'flex',
flexDirection: 'column',
minWidth: 0,
minHeight: '100%',
position: 'relative',
flex: '1 1 auto',
width: '100%',
height: 'auto',
backgroundColor: theme.palette.background.default,
'& .FusePageCarded-scroll-content': {
height: '100%',
},
'& .FusePageCarded-wrapper': {
display: 'flex',
flexDirection: 'row',
flex: '1 1 auto',
zIndex: 2,
maxWidth: '100%',
minWidth: 0,
height: '100%',
backgroundColor: theme.palette.background.paper,
...(props.scroll === 'content' && {
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
left: 0,
overflow: 'hidden',
}),
},
'& .FusePageCarded-header': {
display: 'flex',
flex: '0 0 auto',
},
'& .FusePageCarded-contentWrapper': {
display: 'flex',
flexDirection: 'column',
flex: '1 1 auto',
overflow: 'auto',
WebkitOverflowScrolling: 'touch',
zIndex: 9999,
},
'& .FusePageCarded-toolbar': {
height: toolbarHeight,
minHeight: toolbarHeight,
display: 'flex',
alignItems: 'center',
},
'& .FusePageCarded-content': {
flex: '1 0 auto',
},
'& .FusePageCarded-sidebarWrapper': {
overflow: 'hidden',
backgroundColor: 'transparent',
position: 'absolute',
'&.permanent': {
[theme.breakpoints.up('lg')]: {
position: 'relative',
marginLeft: 0,
marginRight: 0,
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
'&.closed': {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
'&.FusePageCarded-leftSidebar': {
marginLeft: -props.leftsidebarwidth,
},
'&.FusePageCarded-rightSidebar': {
marginRight: -props.rightsidebarwidth,
},
},
},
},
},
'& .FusePageCarded-sidebar': {
position: 'absolute',
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
'&.permanent': {
[theme.breakpoints.up('lg')]: {
position: 'relative',
},
},
maxWidth: '100%',
height: '100%',
},
'& .FusePageCarded-leftSidebar': {
width: props.leftsidebarwidth,
[theme.breakpoints.up('lg')]: {
// borderRight: `1px solid ${theme.palette.divider}`,
// borderLeft: 0,
},
},
'& .FusePageCarded-rightSidebar': {
width: props.rightsidebarwidth,
[theme.breakpoints.up('lg')]: {
// borderLeft: `1px solid ${theme.palette.divider}`,
// borderRight: 0,
},
},
'& .FusePageCarded-sidebarHeader': {
height: headerHeight,
minHeight: headerHeight,
backgroundColor: theme.palette.primary.dark,
color: theme.palette.primary.contrastText,
},
'& .FusePageCarded-sidebarHeaderInnerSidebar': {
backgroundColor: 'transparent',
color: 'inherit',
height: 'auto',
minHeight: 'auto',
},
'& .FusePageCarded-sidebarContent': {
display: 'flex',
flexDirection: 'column',
minHeight: '100%',
},
'& .FusePageCarded-backdrop': {
position: 'absolute',
},
}));
const headerHeight = 120;
const toolbarHeight = 64;
const FusePageCarded = forwardRef((props, ref) => {
// console.info("render::FusePageCarded");
const leftSidebarRef = useRef(null);
const rightSidebarRef = useRef(null);
const rootRef = useRef(null);
useImperativeHandle(ref, () => ({
rootRef,
toggleLeftSidebar: (val) => {
leftSidebarRef.current.toggleSidebar(val);
},
toggleRightSidebar: (val) => {
rightSidebarRef.current.toggleSidebar(val);
},
}));
return (
<>
<GlobalStyles
styles={(theme) => ({
...(props.scroll !== 'page' && {
'#fuse-toolbar': {
position: 'static',
},
'#fuse-footer': {
position: 'static',
},
}),
...(props.scroll === 'page' && {
'#fuse-toolbar': {
position: 'sticky',
top: 0,
},
'#fuse-footer': {
position: 'sticky',
bottom: 0,
},
}),
})}
/>
<Root
className={clsx(
'FusePageCarded-root',
`FusePageCarded-scroll-${props.scroll}`,
props.className
)}
ref={rootRef}
scroll={props.scroll}
leftsidebarwidth={props.leftSidebarWidth}
rightsidebarwidth={props.rightSidebarWidth}
>
{props.header && <FusePageCardedHeader header={props.header} />}
<div className="flex flex-auto flex-col container z-10 h-full shadow-1 rounded-t-16 relative overflow-hidden">
<div className="FusePageCarded-wrapper">
{props.leftSidebarContent && (
<FusePageCardedSidebar
position="left"
content={props.leftSidebarContent}
variant={props.leftSidebarVariant || 'permanent'}
ref={leftSidebarRef}
rootRef={rootRef}
open={props.leftSidebarOpen}
onClose={props.leftSidebarOnClose}
sidebarWidth={props.leftSidebarWidth}
/>
)}
<FuseScrollbars
className="FusePageCarded-contentWrapper"
enable={props.scroll === 'content'}
>
{props.content && (
<div className={clsx('FusePageCarded-content')}>{props.content}</div>
)}
</FuseScrollbars>
{props.rightSidebarContent && (
<FusePageCardedSidebar
position="right"
content={props.rightSidebarContent}
variant={props.rightSidebarVariant || 'permanent'}
ref={rightSidebarRef}
rootRef={rootRef}
open={props.rightSidebarOpen}
onClose={props.rightSidebarOnClose}
sidebarWidth={props.rightSidebarWidth}
/>
)}
</div>
</div>
</Root>
</>
);
});
FusePageCarded.propTypes = {
leftSidebarHeader: PropTypes.node,
leftSidebarContent: PropTypes.node,
leftSidebarVariant: PropTypes.node,
rightSidebarContent: PropTypes.node,
rightSidebarVariant: PropTypes.node,
header: PropTypes.node,
content: PropTypes.node,
contentToolbar: PropTypes.node,
scroll: PropTypes.oneOf(['normal', 'page', 'content']),
leftSidebarOpen: PropTypes.bool,
rightSidebarOpen: PropTypes.bool,
leftSidebarWidth: PropTypes.number,
rightSidebarWidth: PropTypes.number,
rightSidebarOnClose: PropTypes.func,
leftSidebarOnClose: PropTypes.func,
};
FusePageCarded.defaultProps = {
classes: {},
scroll: 'page',
leftSidebarOpen: false,
rightSidebarOpen: false,
rightSidebarWidth: 240,
leftSidebarWidth: 240,
};
export default memo(styled(FusePageCarded)``);

View File

@@ -0,0 +1,9 @@
import clsx from 'clsx';
function FusePageCardedHeader(props) {
return (
<div className={clsx('FusePageCarded-header', 'container')}>{props.header && props.header}</div>
);
}
export default FusePageCardedHeader;

View File

@@ -0,0 +1,92 @@
import Drawer from '@mui/material/Drawer';
import Hidden from '@mui/material/Hidden';
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import clsx from 'clsx';
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react';
import FusePageCardedSidebarContent from './FusePageCardedSidebarContent';
const FusePageCardedSidebar = forwardRef((props, ref) => {
const { open, position, variant, rootRef, sidebarWidth } = props;
const [isOpen, setIsOpen] = useState(open);
useImperativeHandle(ref, () => ({
toggleSidebar: handleToggleDrawer,
}));
const handleToggleDrawer = useCallback((val) => {
setIsOpen(val);
}, []);
useEffect(() => {
handleToggleDrawer(open);
}, [handleToggleDrawer, open]);
return (
<>
<Hidden lgUp={variant === 'permanent'}>
<SwipeableDrawer
variant="temporary"
anchor={position}
open={isOpen}
onOpen={(ev) => {}}
onClose={() => props?.onClose()}
disableSwipeToOpen
classes={{
root: clsx('FusePageCarded-sidebarWrapper', variant),
paper: clsx(
'FusePageCarded-sidebar',
variant,
position === 'left' ? 'FusePageCarded-leftSidebar' : 'FusePageCarded-rightSidebar'
),
}}
sx={{
'& .MuiPaper-root': {
width: sidebarWidth,
maxWidth: '100%',
},
}}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
// container={rootRef.current}
BackdropProps={{
classes: {
root: 'FusePageCarded-backdrop',
},
}}
style={{ position: 'absolute' }}
>
<FusePageCardedSidebarContent {...props} />
</SwipeableDrawer>
</Hidden>
{variant === 'permanent' && (
<Hidden lgDown>
<Drawer
variant="permanent"
anchor={position}
className={clsx(
'FusePageCarded-sidebarWrapper',
variant,
isOpen ? 'opened' : 'closed',
position === 'left' ? 'FusePageCarded-leftSidebar' : 'FusePageCarded-rightSidebar'
)}
open={isOpen}
onClose={props?.onClose}
classes={{
paper: clsx('FusePageCarded-sidebar', variant),
}}
>
<FusePageCardedSidebarContent {...props} />
</Drawer>
</Hidden>
)}
</>
);
});
FusePageCardedSidebar.defaultProps = {
open: true,
};
export default FusePageCardedSidebar;

View File

@@ -0,0 +1,32 @@
import { useSelector } from 'react-redux';
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { ThemeProvider, useTheme } from '@mui/material/styles';
import { selectContrastMainTheme } from 'app/store/fuse/settingsSlice';
import clsx from 'clsx';
function FusePageCardedSidebarContent(props) {
const theme = useTheme();
const contrastTheme = useSelector(selectContrastMainTheme(theme.palette.primary.main));
return (
<FuseScrollbars enable={props.innerScroll}>
{props.header && (
<ThemeProvider theme={contrastTheme}>
<div
className={clsx(
'FusePageCarded-sidebarHeader',
props.variant,
props.sidebarInner && 'FusePageCarded-sidebarHeaderInnerSidebar'
)}
>
{props.header}
</div>
</ThemeProvider>
)}
{props.content && <div className="FusePageCarded-sidebarContent">{props.content}</div>}
</FuseScrollbars>
);
}
export default FusePageCardedSidebarContent;

View File

@@ -0,0 +1 @@
export { default } from './FusePageCarded';

View File

@@ -0,0 +1,299 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import * as PropTypes from 'prop-types';
import { forwardRef, memo, useImperativeHandle, useRef } from 'react';
import GlobalStyles from '@mui/material/GlobalStyles';
import FusePageSimpleHeader from './FusePageSimpleHeader';
import FusePageSimpleSidebar from './FusePageSimpleSidebar';
const Root = styled('div')(({ theme, ...props }) => ({
display: 'flex',
flexDirection: 'column',
minWidth: 0,
minHeight: '100%',
position: 'relative',
flex: '1 1 auto',
width: '100%',
height: 'auto',
backgroundColor: theme.palette.background.default,
'&.FusePageSimple-scroll-content': {
height: '100%',
},
'& .FusePageSimple-wrapper': {
display: 'flex',
flexDirection: 'row',
flex: '1 1 auto',
zIndex: 2,
minWidth: 0,
height: '100%',
backgroundColor: theme.palette.background.default,
...(props.scroll === 'content' && {
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
left: 0,
overflow: 'hidden',
}),
},
'& .FusePageSimple-header': {
display: 'flex',
flex: '0 0 auto',
backgroundSize: 'cover',
},
'& .FusePageSimple-topBg': {
position: 'absolute',
left: 0,
right: 0,
top: 0,
height: headerHeight,
pointerEvents: 'none',
},
'& .FusePageSimple-contentWrapper': {
display: 'flex',
flexDirection: 'column',
width: '100%',
flex: '1 1 auto',
overflow: 'hidden',
// WebkitOverflowScrolling: 'touch',
zIndex: 9999,
},
'& .FusePageSimple-toolbar': {
height: toolbarHeight,
minHeight: toolbarHeight,
display: 'flex',
alignItems: 'center',
},
'& .FusePageSimple-content': {
display: 'flex',
flex: '1 1 auto',
alignItems: 'start',
minHeight: 0,
overflowY: 'auto',
},
'& .FusePageSimple-sidebarWrapper': {
overflow: 'hidden',
backgroundColor: 'transparent',
position: 'absolute',
'&.permanent': {
[theme.breakpoints.up('lg')]: {
position: 'relative',
marginLeft: 0,
marginRight: 0,
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
'&.closed': {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
'&.FusePageSimple-leftSidebar': {
marginLeft: -props.leftsidebarwidth,
},
'&.FusePageSimple-rightSidebar': {
marginRight: -props.rightsidebarwidth,
},
},
},
},
},
'& .FusePageSimple-sidebar': {
position: 'absolute',
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
'&.permanent': {
[theme.breakpoints.up('lg')]: {
position: 'relative',
},
},
maxWidth: '100%',
height: '100%',
},
'& .FusePageSimple-leftSidebar': {
width: props.leftsidebarwidth,
[theme.breakpoints.up('lg')]: {
borderRight: `1px solid ${theme.palette.divider}`,
borderLeft: 0,
},
},
'& .FusePageSimple-rightSidebar': {
width: props.rightsidebarwidth,
[theme.breakpoints.up('lg')]: {
borderLeft: `1px solid ${theme.palette.divider}`,
borderRight: 0,
},
},
'& .FusePageSimple-sidebarHeader': {
height: headerHeight,
minHeight: headerHeight,
backgroundColor: theme.palette.primary.dark,
color: theme.palette.primary.contrastText,
},
'& .FusePageSimple-sidebarHeaderInnerSidebar': {
backgroundColor: 'transparent',
color: 'inherit',
height: 'auto',
minHeight: 'auto',
},
'& .FusePageSimple-sidebarContent': {
display: 'flex',
flexDirection: 'column',
minHeight: '100%',
},
'& .FusePageSimple-backdrop': {
position: 'absolute',
},
}));
const headerHeight = 120;
const toolbarHeight = 64;
const FusePageSimple = forwardRef((props, ref) => {
// console.info("render::FusePageSimple");
const leftSidebarRef = useRef(null);
const rightSidebarRef = useRef(null);
const rootRef = useRef(null);
useImperativeHandle(ref, () => ({
rootRef,
toggleLeftSidebar: (val) => {
leftSidebarRef.current.toggleSidebar(val);
},
toggleRightSidebar: (val) => {
rightSidebarRef.current.toggleSidebar(val);
},
}));
return (
<>
<GlobalStyles
styles={(theme) => ({
...(props.scroll !== 'page' && {
'#fuse-toolbar': {
position: 'static',
},
'#fuse-footer': {
position: 'static',
},
}),
...(props.scroll === 'page' && {
'#fuse-toolbar': {
position: 'sticky',
top: 0,
},
'#fuse-footer': {
position: 'sticky',
bottom: 0,
},
}),
})}
/>
<Root
className={clsx(
'FusePageSimple-root',
`FusePageSimple-scroll-${props.scroll}`,
props.className
)}
ref={rootRef}
scroll={props.scroll}
leftsidebarwidth={props.leftSidebarWidth}
rightsidebarwidth={props.rightSidebarWidth}
>
<div className="flex flex-auto flex-col z-10 h-full">
<div className="FusePageSimple-wrapper">
{props.leftSidebarContent && (
<FusePageSimpleSidebar
position="left"
content={props.leftSidebarContent}
variant={props.leftSidebarVariant || 'permanent'}
ref={leftSidebarRef}
rootRef={rootRef}
open={props.leftSidebarOpen}
onClose={props.leftSidebarOnClose}
sidebarWidth={props.leftSidebarWidth}
/>
)}
<div
className="FusePageSimple-contentWrapper"
// enable={props.scroll === 'page'}
>
{props.header && <FusePageSimpleHeader header={props.header} />}
{props.content && (
<FuseScrollbars
enable={props.scroll === 'content'}
className={clsx('FusePageSimple-content container')}
>
{props.content}
</FuseScrollbars>
)}
</div>
{props.rightSidebarContent && (
<FusePageSimpleSidebar
position="right"
content={props.rightSidebarContent}
variant={props.rightSidebarVariant || 'permanent'}
ref={rightSidebarRef}
rootRef={rootRef}
open={props.rightSidebarOpen}
onClose={props.rightSidebarOnClose}
sidebarWidth={props.rightSidebarWidth}
/>
)}
</div>
</div>
</Root>
</>
);
});
FusePageSimple.propTypes = {
leftSidebarContent: PropTypes.node,
leftSidebarVariant: PropTypes.node,
rightSidebarContent: PropTypes.node,
rightSidebarVariant: PropTypes.node,
header: PropTypes.node,
content: PropTypes.node,
scroll: PropTypes.oneOf(['normal', 'page', 'content']),
leftSidebarOpen: PropTypes.bool,
rightSidebarOpen: PropTypes.bool,
leftSidebarWidth: PropTypes.number,
rightSidebarWidth: PropTypes.number,
rightSidebarOnClose: PropTypes.func,
leftSidebarOnClose: PropTypes.func,
};
FusePageSimple.defaultProps = {
classes: {},
scroll: 'page',
leftSidebarOpen: false,
rightSidebarOpen: false,
rightSidebarWidth: 240,
leftSidebarWidth: 240,
};
export default memo(styled(FusePageSimple)``);

View File

@@ -0,0 +1,11 @@
import clsx from 'clsx';
function FusePageSimpleHeader(props) {
return (
<div className={clsx('FusePageSimple-header', props.className)}>
<div className="container">{props.header && props.header}</div>
</div>
);
}
export default FusePageSimpleHeader;

View File

@@ -0,0 +1,93 @@
import Drawer from '@mui/material/Drawer';
import Hidden from '@mui/material/Hidden';
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import clsx from 'clsx';
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react';
import FusePageSimpleSidebarContent from './FusePageSimpleSidebarContent';
const FusePageSimpleSidebar = forwardRef((props, ref) => {
const { open, position, variant, rootRef, sidebarWidth } = props;
const [isOpen, setIsOpen] = useState(open);
useImperativeHandle(ref, () => ({
toggleSidebar: handleToggleDrawer,
}));
const handleToggleDrawer = useCallback((val) => {
setIsOpen(val);
}, []);
useEffect(() => {
handleToggleDrawer(open);
}, [handleToggleDrawer, open]);
return (
<>
<Hidden lgUp={variant === 'permanent'}>
<SwipeableDrawer
variant="temporary"
anchor={position}
open={isOpen}
onOpen={(ev) => {}}
onClose={() => props?.onClose()}
disableSwipeToOpen
classes={{
root: clsx('FusePageSimple-sidebarWrapper', variant),
paper: clsx(
'FusePageSimple-sidebar',
variant,
position === 'left' ? 'FusePageSimple-leftSidebar' : 'FusePageSimple-rightSidebar'
),
}}
sx={{
'& .MuiPaper-root': {
width: sidebarWidth,
maxWidth: '100%',
},
}}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
// container={rootRef.current}
BackdropProps={{
classes: {
root: 'FusePageSimple-backdrop',
},
}}
style={{ position: 'absolute' }}
>
<FusePageSimpleSidebarContent {...props} />
</SwipeableDrawer>
</Hidden>
{variant === 'permanent' && (
<Hidden lgDown>
<Drawer
variant="permanent"
anchor={position}
className={clsx(
'FusePageSimple-sidebarWrapper',
variant,
isOpen ? 'opened' : 'closed',
position === 'left' ? 'FusePageSimple-leftSidebar' : 'FusePageSimple-rightSidebar'
)}
open={isOpen}
onClose={props?.onClose}
classes={{
paper: clsx('FusePageSimple-sidebar border-0', variant),
}}
>
<FusePageSimpleSidebarContent {...props} />
</Drawer>
</Hidden>
)}
</>
);
});
FusePageSimpleSidebar.defaultProps = {
open: true,
};
export default FusePageSimpleSidebar;

View File

@@ -0,0 +1,24 @@
import { useSelector } from 'react-redux';
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { ThemeProvider, useTheme } from '@mui/material/styles';
import { selectContrastMainTheme } from 'app/store/fuse/settingsSlice';
import clsx from 'clsx';
function FusePageSimpleSidebarContent(props) {
const theme = useTheme();
const contrastTheme = useSelector(selectContrastMainTheme(theme.palette.primary.main));
return (
<FuseScrollbars enable={props.innerScroll}>
{props.header && (
<ThemeProvider theme={contrastTheme}>
<div className={clsx('FusePageSimple-sidebarHeader', props.variant)}>{props.header}</div>
</ThemeProvider>
)}
{props.content && <div className="FusePageSimple-sidebarContent">{props.content}</div>}
</FuseScrollbars>
);
}
export default FusePageSimpleSidebarContent;

View File

@@ -0,0 +1 @@
export { default } from './FusePageSimple';

View File

@@ -0,0 +1,197 @@
import { styled } from '@mui/material/styles';
import MobileDetect from 'mobile-detect';
import PerfectScrollbar from 'perfect-scrollbar';
import 'perfect-scrollbar/css/perfect-scrollbar.css';
import PropTypes from 'prop-types';
import { createRef, forwardRef, useCallback, useEffect, useRef } from 'react';
import { connect } from 'react-redux';
import history from '@history';
import withRouterAndRef from '../withRouterAndRef/withRouterAndRef';
const Root = styled('div')(({ theme }) => ({
overscrollBehavior: 'contain',
minHeight: '100%',
}));
const md = new MobileDetect(window.navigator.userAgent);
const isMobile = md.mobile();
const handlerNameByEvent = {
'ps-scroll-y': 'onScrollY',
'ps-scroll-x': 'onScrollX',
'ps-scroll-up': 'onScrollUp',
'ps-scroll-down': 'onScrollDown',
'ps-scroll-left': 'onScrollLeft',
'ps-scroll-right': 'onScrollRight',
'ps-y-reach-start': 'onYReachStart',
'ps-y-reach-end': 'onYReachEnd',
'ps-x-reach-start': 'onXReachStart',
'ps-x-reach-end': 'onXReachEnd',
};
Object.freeze(handlerNameByEvent);
const FuseScrollbars = forwardRef((props, ref) => {
ref = ref || createRef();
const ps = useRef(null);
const handlerByEvent = useRef(new Map());
const { customScrollbars } = props;
const hookUpEvents = useCallback(() => {
Object.keys(handlerNameByEvent).forEach((key) => {
const callback = props[handlerNameByEvent[key]];
if (callback) {
const handler = () => callback(ref.current);
handlerByEvent.current.set(key, handler);
ref.current.addEventListener(key, handler, false);
}
});
// eslint-disable-next-line
}, [ref]);
const unHookUpEvents = useCallback(() => {
handlerByEvent.current.forEach((value, key) => {
if (ref.current) {
ref.current.removeEventListener(key, value, false);
}
});
handlerByEvent.current.clear();
}, [ref]);
const destroyPs = useCallback(() => {
// console.info("destroy::ps");
unHookUpEvents();
if (!ps.current) {
return;
}
ps.current.destroy();
ps.current = null;
}, [unHookUpEvents]);
const createPs = useCallback(() => {
// console.info("create::ps");
if (isMobile || !ref || ps.current) {
return;
}
ps.current = new PerfectScrollbar(ref.current, props.option);
hookUpEvents();
}, [hookUpEvents, props.option, ref]);
useEffect(() => {
function updatePs() {
if (!ps.current) {
return;
}
ps.current.update();
}
updatePs();
});
useEffect(() => {
if (customScrollbars) {
createPs();
} else {
destroyPs();
}
}, [createPs, customScrollbars, destroyPs]);
const scrollToTop = useCallback(() => {
if (ref && ref.current) {
ref.current.scrollTop = 0;
}
}, [ref]);
useEffect(() => {
if (props.scrollToTopOnChildChange) {
scrollToTop();
}
}, [scrollToTop, props.children, props.scrollToTopOnChildChange]);
useEffect(
() =>
history.listen(() => {
if (props.scrollToTopOnRouteChange) {
scrollToTop();
}
}),
[scrollToTop, props.scrollToTopOnRouteChange]
);
useEffect(
() => () => {
destroyPs();
},
[destroyPs]
);
// console.info('render::ps');
return (
<Root
id={props.id}
className={props.className}
style={
props.customScrollbars && (props.enable || true) && !isMobile
? {
position: 'relative',
overflow: 'hidden!important',
}
: {}
}
ref={ref}
>
{props.children}
</Root>
);
});
function mapStateToProps({ fuse }) {
return {
customScrollbars: fuse.settings.current.customScrollbars,
};
}
FuseScrollbars.propTypes = {
onScrollY: PropTypes.func,
onScrollX: PropTypes.func,
onScrollUp: PropTypes.func,
onScrollDown: PropTypes.func,
onScrollLeft: PropTypes.func,
onScrollRight: PropTypes.func,
onYReachStart: PropTypes.func,
onYReachEnd: PropTypes.func,
onXReachStart: PropTypes.func,
onXReachEnd: PropTypes.func,
scrollToTopOnRouteChange: PropTypes.bool,
scrollToTopOnChildChange: PropTypes.bool,
};
FuseScrollbars.defaultProps = {
className: '',
enable: true,
scrollToTopOnChildChange: false,
scrollToTopOnRouteChange: false,
option: {
wheelPropagation: true,
},
ref: undefined,
onScrollY: undefined,
onScrollX: undefined,
onScrollUp: undefined,
onScrollDown: undefined,
onScrollLeft: undefined,
onScrollRight: undefined,
onYReachStart: undefined,
onYReachEnd: undefined,
onXReachStart: undefined,
onXReachEnd: undefined,
};
export default connect(mapStateToProps, null, null, { forwardRef: true })(
withRouterAndRef(FuseScrollbars)
);

View File

@@ -0,0 +1 @@
export { default } from './FuseScrollbars';

View File

@@ -0,0 +1,440 @@
import ClickAwayListener from '@mui/material/ClickAwayListener';
import { styled } from '@mui/material/styles';
import IconButton from '@mui/material/IconButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import MenuItem from '@mui/material/MenuItem';
import Paper from '@mui/material/Paper';
import Popper from '@mui/material/Popper';
import TextField from '@mui/material/TextField';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import match from 'autosuggest-highlight/match';
import parse from 'autosuggest-highlight/parse';
import clsx from 'clsx';
import _ from '@lodash';
import { memo, useEffect, useReducer, useRef } from 'react';
import Autosuggest from 'react-autosuggest';
import withRouter from '@fuse/core/withRouter';
import FuseSvgIcon from '../FuseSvgIcon';
const Root = styled('div')(({ theme }) => ({
'& .FuseSearch-container': {
position: 'relative',
},
'& .FuseSearch-suggestionsContainerOpen': {
position: 'absolute',
zIndex: 1,
marginTop: theme.spacing(),
left: 0,
right: 0,
},
'& .FuseSearch-suggestion': {
display: 'block',
},
'& .FuseSearch-suggestionsList': {
margin: 0,
padding: 0,
listStyleType: 'none',
},
'& .FuseSearch-input': {
transition: theme.transitions.create(['background-color'], {
easing: theme.transitions.easing.easeInOut,
duration: theme.transitions.duration.short,
}),
'&:focus': {
backgroundColor: theme.palette.background.paper,
},
},
}));
function renderInputComponent(inputProps) {
const { variant, inputRef = () => {}, ref, ...other } = inputProps;
return (
<div className="w-full relative">
{variant === 'basic' ? (
// Outlined
<>
<TextField
fullWidth
InputProps={{
inputRef: (node) => {
ref(node);
inputRef(node);
},
classes: {
input: 'FuseSearch-input py-0 px-16 h-40 md:h-48 ltr:pr-48 rtl:pl-48',
notchedOutline: 'rounded-8',
},
}}
variant="outlined"
{...other}
/>
<FuseSvgIcon
className="absolute top-0 ltr:right-0 rtl:left-0 h-40 md:h-48 w-48 p-12 pointer-events-none"
color="action"
>
heroicons-outline:search
</FuseSvgIcon>
</>
) : (
// Standard
<TextField
fullWidth
InputProps={{
disableUnderline: true,
inputRef: (node) => {
ref(node);
inputRef(node);
},
classes: {
input: 'FuseSearch-input py-0 px-16 h-48 md:h-64',
},
}}
variant="standard"
{...other}
/>
)}
</div>
);
}
function renderSuggestion(suggestion, { query, isHighlighted }) {
const matches = match(suggestion.title, query);
const parts = parse(suggestion.title, matches);
return (
<MenuItem selected={isHighlighted} component="div">
<ListItemIcon className="min-w-40">
{suggestion.icon ? (
<FuseSvgIcon>{suggestion.icon}</FuseSvgIcon>
) : (
<span className="text-20 w-24 font-semibold uppercase text-center">
{suggestion.title[0]}
</span>
)}
</ListItemIcon>
<ListItemText
primary={parts.map((part, index) =>
part.highlight ? (
<span key={String(index)} style={{ fontWeight: 600 }}>
{part.text}
</span>
) : (
<strong key={String(index)} style={{ fontWeight: 300 }}>
{part.text}
</strong>
)
)}
/>
</MenuItem>
);
}
function getSuggestions(value, data) {
const inputValue = _.deburr(value.trim()).toLowerCase();
const inputLength = inputValue.length;
let count = 0;
return inputLength === 0
? []
: data.filter((suggestion) => {
const keep = count < 10 && match(suggestion.title, inputValue).length > 0;
if (keep) {
count += 1;
}
return keep;
});
}
function getSuggestionValue(suggestion) {
return suggestion.title;
}
const initialState = {
searchText: '',
search: false,
navigation: null,
suggestions: [],
noSuggestions: false,
};
function reducer(state, action) {
switch (action.type) {
case 'open': {
return {
...state,
opened: true,
};
}
case 'close': {
return {
...state,
opened: false,
searchText: '',
};
}
case 'setSearchText': {
return {
...state,
searchText: action.value,
};
}
case 'setNavigation': {
return {
...state,
navigation: action.value,
};
}
case 'updateSuggestions': {
const suggestions = getSuggestions(action.value, state.navigation);
const isInputBlank = action.value.trim() === '';
const noSuggestions = !isInputBlank && suggestions.length === 0;
return {
...state,
suggestions,
noSuggestions,
};
}
case 'clearSuggestions': {
return {
...state,
suggestions: [],
noSuggestions: false,
};
}
case 'decrement': {
return { count: state.count - 1 };
}
default: {
throw new Error();
}
}
}
function FuseSearch(props) {
const { navigation } = props;
const [state, dispatch] = useReducer(reducer, initialState);
const suggestionsNode = useRef(null);
const popperNode = useRef(null);
const buttonNode = useRef(null);
useEffect(() => {
dispatch({
type: 'setNavigation',
value: navigation,
});
}, [navigation]);
function showSearch(ev) {
ev.stopPropagation();
dispatch({ type: 'open' });
document.addEventListener('keydown', escFunction, false);
}
function hideSearch() {
dispatch({ type: 'close' });
document.removeEventListener('keydown', escFunction, false);
}
function escFunction(event) {
if (event.keyCode === 27) {
hideSearch();
}
}
function handleSuggestionsFetchRequested({ value }) {
dispatch({
type: 'updateSuggestions',
value,
});
}
function handleSuggestionSelected(event, { suggestion }) {
event.preventDefault();
event.stopPropagation();
if (!suggestion.url) {
return;
}
props.navigate(suggestion.url);
hideSearch();
}
function handleSuggestionsClearRequested() {
dispatch({
type: 'clearSuggestions',
});
}
function handleChange(event) {
dispatch({
type: 'setSearchText',
value: event.target.value,
});
}
function handleClickAway(event) {
return (
state.opened &&
(!suggestionsNode.current || !suggestionsNode.current.contains(event.target)) &&
hideSearch()
);
}
const autosuggestProps = {
renderInputComponent,
highlightFirstSuggestion: true,
suggestions: state.suggestions,
onSuggestionsFetchRequested: handleSuggestionsFetchRequested,
onSuggestionsClearRequested: handleSuggestionsClearRequested,
onSuggestionSelected: handleSuggestionSelected,
getSuggestionValue,
renderSuggestion,
};
switch (props.variant) {
case 'basic': {
return (
<div className={clsx('flex items-center w-full', props.className)} ref={popperNode}>
<Autosuggest
{...autosuggestProps}
inputProps={{
variant: props.variant,
placeholder: props.placeholder,
value: state.searchText,
onChange: handleChange,
onFocus: showSearch,
InputLabelProps: {
shrink: true,
},
autoFocus: false,
}}
theme={{
container: 'flex flex-1 w-full',
suggestionsList: 'FuseSearch-suggestionsList',
suggestion: 'FuseSearch-suggestion',
}}
renderSuggestionsContainer={(options) => (
<Popper
anchorEl={popperNode.current}
open={Boolean(options.children) || state.noSuggestions}
popperOptions={{ positionFixed: true }}
className="z-9999"
>
<div ref={suggestionsNode}>
<Paper
className="shadow-lg rounded-8 overflow-hidden"
{...options.containerProps}
style={{ width: popperNode.current ? popperNode.current.clientWidth : null }}
>
{options.children}
{state.noSuggestions && (
<Typography className="px-16 py-12">{props.noResults}</Typography>
)}
</Paper>
</div>
</Popper>
)}
/>
</div>
);
}
case 'full': {
return (
<Root className={clsx('flex', props.className)}>
<Tooltip title="Click to search" placement="bottom">
<div
onClick={showSearch}
onKeyDown={showSearch}
role="button"
tabIndex={0}
ref={buttonNode}
>
{props.trigger}
</div>
</Tooltip>
{state.opened && (
<ClickAwayListener onClickAway={handleClickAway}>
<Paper className="absolute left-0 right-0 top-0 h-full z-9999 shadow-0" square>
<div className="flex items-center w-full h-full" ref={popperNode}>
<Autosuggest
{...autosuggestProps}
inputProps={{
placeholder: props.placeholder,
value: state.searchText,
onChange: handleChange,
InputLabelProps: {
shrink: true,
},
autoFocus: true,
}}
theme={{
container: 'flex flex-1 w-full',
suggestionsList: 'FuseSearch-suggestionsList',
suggestion: 'FuseSearch-suggestion',
}}
renderSuggestionsContainer={(options) => (
<Popper
anchorEl={popperNode.current}
open={Boolean(options.children) || state.noSuggestions}
popperOptions={{ positionFixed: true }}
className="z-9999"
>
<div ref={suggestionsNode}>
<Paper
className="shadow-lg"
square
{...options.containerProps}
style={{
width: popperNode.current ? popperNode.current.clientWidth : null,
}}
>
{options.children}
{state.noSuggestions && (
<Typography className="px-16 py-12">{props.noResults}</Typography>
)}
</Paper>
</div>
</Popper>
)}
/>
<IconButton onClick={hideSearch} className="mx-8" size="large">
<FuseSvgIcon>heroicons-outline:x</FuseSvgIcon>
</IconButton>
</div>
</Paper>
</ClickAwayListener>
)}
</Root>
);
}
default: {
return null;
}
}
}
FuseSearch.propTypes = {};
FuseSearch.defaultProps = {
navigation: [],
trigger: (
<IconButton className="w-40 h-40" size="large">
<FuseSvgIcon>heroicons-outline:search</FuseSvgIcon>
</IconButton>
),
variant: 'full',
placeholder: 'Search',
noResults: 'No results..',
};
export default withRouter(memo(FuseSearch));

View File

@@ -0,0 +1 @@
export { default } from './FuseSearch';

View File

@@ -0,0 +1,464 @@
import { useDebounce, usePrevious } from '@fuse/hooks';
import { styled } from '@mui/material/styles';
import { Controller, useForm } from 'react-hook-form';
import themeLayoutConfigs from 'app/theme-layouts/themeLayoutConfigs';
import _ from '@lodash';
import TextField from '@mui/material/TextField';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormLabel from '@mui/material/FormLabel';
import MenuItem from '@mui/material/MenuItem';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import Select from '@mui/material/Select';
import Switch from '@mui/material/Switch';
import Typography from '@mui/material/Typography';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
selectFuseCurrentSettings,
selectFuseThemesSettings,
setDefaultSettings,
} from 'app/store/fuse/settingsSlice';
import { selectUser } from 'app/store/userSlice';
import PaletteSelector from './palette-generator/PaletteSelector';
import SectionPreview from './palette-generator/SectionPreview';
const Root = styled('div')(({ theme }) => ({
'& .FuseSettings-formControl': {
margin: '6px 0',
width: '100%',
'&:last-child': {
marginBottom: 0,
},
},
'& .FuseSettings-group': {},
'& .FuseSettings-formGroupTitle': {
position: 'absolute',
top: -10,
left: 8,
fontWeight: 600,
padding: '0 4px',
backgroundColor: theme.palette.background.paper,
},
'& .FuseSettings-formGroup': {
position: 'relative',
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2,
padding: '12px 12px 0 12px',
margin: '24px 0 16px 0',
'&:first-of-type': {
marginTop: 16,
},
},
}));
function FuseSettings(props) {
const dispatch = useDispatch();
const user = useSelector(selectUser);
const themes = useSelector(selectFuseThemesSettings);
const settings = useSelector(selectFuseCurrentSettings);
const { reset, watch, control } = useForm({
mode: 'onChange',
defaultValues: settings,
});
const form = watch();
const { form: formConfigs } = themeLayoutConfigs[form.layout.style];
const prevForm = usePrevious(form ? _.merge({}, form) : null);
const prevSettings = usePrevious(settings ? _.merge({}, settings) : null);
const formChanged = !_.isEqual(form, prevForm);
const settingsChanged = !_.isEqual(settings, prevSettings);
const handleUpdate = useDebounce((newSettings) => {
dispatch(setDefaultSettings(newSettings));
}, 300);
useEffect(() => {
// Skip inital changes
if (!prevForm && !prevSettings) {
return;
}
// If theme settings changed update form data
if (settingsChanged) {
reset(settings);
return;
}
const newSettings = _.merge({}, settings, form);
// No need to change
if (_.isEqual(newSettings, settings)) {
return;
}
// If form changed update theme settings
if (formChanged) {
if (settings.layout.style !== newSettings.layout.style) {
_.set(
newSettings,
'layout.config',
themeLayoutConfigs[newSettings?.layout?.style]?.defaults
);
}
handleUpdate(newSettings);
}
}, [
dispatch,
form,
formChanged,
handleUpdate,
prevForm,
prevSettings,
reset,
settings,
settingsChanged,
user,
]);
const ThemeSelect = ({ value, name, handleThemeChange }) => {
return (
<Select
className="w-full rounded-8 h-40 overflow-hidden my-8"
value={value}
onChange={handleThemeChange}
name={name}
variant="outlined"
style={{
backgroundColor: themes[value].palette.background.default,
color: themes[value].palette.mode === 'light' ? '#000000' : '#ffffff',
}}
>
{Object.entries(themes)
.filter(
([key, val]) =>
!(name === 'theme.main' && (key === 'mainThemeDark' || key === 'mainThemeLight'))
)
.map(([key, val]) => (
<MenuItem
key={key}
value={key}
className="m-8 mt-0 rounded-lg"
style={{
backgroundColor: val.palette.background.default,
color: val.palette.mode === 'light' ? '#000000' : '#FFFFFF',
border: `1px solid ${
val.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.12)' : 'rgba(255, 255, 255, 0.12)'
}`,
}}
>
{_.startCase(key)}
<div
className="flex w-full h-8 block absolute bottom-0 left-0 right-0"
style={{
borderTop: `1px solid ${
val.palette.mode === 'light'
? 'rgba(0, 0, 0, 0.12)'
: 'rgba(255, 255, 255, 0.12)'
}`,
}}
>
<div
className="w-1/4 h-8"
style={{
backgroundColor: val.palette.primary.main
? val.palette.primary.main
: val.palette.primary[500],
}}
/>
<div
className="w-1/4 h-8"
style={{
backgroundColor: val.palette.secondary.main
? val.palette.secondary.main
: val.palette.secondary[500],
}}
/>
<div
className="w-1/4 h-8"
style={{
backgroundColor: val.palette.error.main
? val.palette.error.main
: val.palette.error[500],
}}
/>
<div
className="w-1/4 h-8"
style={{ backgroundColor: val.palette.background.paper }}
/>
</div>
</MenuItem>
))}
</Select>
);
};
const getForm = useCallback(
(_formConfigs, prefix) =>
Object.entries(_formConfigs).map(([key, formControl]) => {
const target = prefix ? `${prefix}.${key}` : key;
switch (formControl.type) {
case 'radio': {
return (
<Controller
key={target}
name={target}
control={control}
render={({ field }) => (
<FormControl component="fieldset" className="FuseSettings-formControl">
<FormLabel component="legend" className="text-14">
{formControl.title}
</FormLabel>
<RadioGroup
{...field}
aria-label={formControl.title}
className="FuseSettings-group"
row={formControl.options.length < 4}
>
{formControl.options.map((opt) => (
<FormControlLabel
key={opt.value}
value={opt.value}
control={<Radio />}
label={opt.name}
/>
))}
</RadioGroup>
</FormControl>
)}
/>
);
}
case 'switch': {
return (
<Controller
key={target}
name={target}
control={control}
render={({ field: { onChange, value } }) => (
<FormControl component="fieldset" className="FuseSettings-formControl">
<FormLabel component="legend" className="text-14">
{formControl.title}
</FormLabel>
<Switch
checked={value}
onChange={(ev) => onChange(ev.target.checked)}
aria-label={formControl.title}
/>
</FormControl>
)}
/>
);
}
case 'number': {
return (
<div key={target} className="FuseSettings-formControl">
<Controller
name={target}
control={control}
render={({ field }) => (
<TextField
{...field}
label={formControl.title}
type="number"
InputLabelProps={{
shrink: true,
}}
variant="outlined"
/>
)}
/>
</div>
);
}
case 'group': {
return (
<div key={target} className="FuseSettings-formGroup">
<Typography className="FuseSettings-formGroupTitle" color="text.secondary">
{formControl.title}
</Typography>
{getForm(formControl.children, target)}
</div>
);
}
default: {
return '';
}
}
}),
[control]
);
return (
<Root>
<div className="FuseSettings-formGroup">
<Typography className="FuseSettings-formGroupTitle" color="text.secondary">
Layout
</Typography>
<Controller
name="layout.style"
control={control}
render={({ field }) => (
<FormControl component="fieldset" className="FuseSettings-formControl">
<FormLabel component="legend" className="text-14">
Style
</FormLabel>
<RadioGroup {...field} aria-label="Layout Style" className="FuseSettings-group">
{Object.entries(themeLayoutConfigs).map(([key, layout]) => (
<FormControlLabel
key={key}
value={key}
control={<Radio />}
label={layout.title}
/>
))}
</RadioGroup>
</FormControl>
)}
/>
{useMemo(() => getForm(formConfigs, 'layout.config'), [formConfigs, getForm])}
<Typography className="my-16 text-12 italic" color="text.secondary">
*Not all option combinations are available
</Typography>
</div>
<div className="FuseSettings-formGroup pb-16">
<Typography className="FuseSettings-formGroupTitle" color="text.secondary">
Theme
</Typography>
<div className="flex flex-wrap -mx-8">
<Controller
name="theme.main"
control={control}
render={({ field: { value, onChange } }) => (
<PaletteSelector
value={value}
onChange={onChange}
trigger={
<div className="flex flex-col items-center space-y-8 w-128 m-8 cursor-pointer group">
<SectionPreview
className="group-hover:shadow-lg transition-shadow"
section="main"
/>
<Typography className="flex-1 text-14 font-semibold mb-24 opacity-80 group-hover:opacity-100">
Main Palette
</Typography>
</div>
}
/>
)}
/>
<Controller
name="theme.navbar"
control={control}
render={({ field: { value, onChange } }) => (
<PaletteSelector
value={value}
onChange={onChange}
trigger={
<div className="flex flex-col items-center space-y-8 w-128 m-8 cursor-pointer group">
<SectionPreview
className="group-hover:shadow-lg transition-shadow"
section="navbar"
/>
<Typography className="flex-1 text-14 font-semibold mb-24 opacity-80 group-hover:opacity-100">
Navbar Palette
</Typography>
</div>
}
/>
)}
/>
<Controller
name="theme.toolbar"
control={control}
render={({ field: { value, onChange } }) => (
<PaletteSelector
value={value}
onChange={onChange}
trigger={
<div className="flex flex-col items-center space-y-8 w-128 m-8 cursor-pointer group">
<SectionPreview
className="group-hover:shadow-lg transition-shadow"
section="toolbar"
/>
<Typography className="flex-1 text-14 font-semibold mb-24 opacity-80 group-hover:opacity-100">
Toolbar Palette
</Typography>
</div>
}
/>
)}
/>
<Controller
name="theme.footer"
control={control}
render={({ field: { value, onChange } }) => (
<PaletteSelector
value={value}
onChange={onChange}
trigger={
<div className="flex flex-col items-center space-y-8 w-128 m-8 cursor-pointer group">
<SectionPreview
className="group-hover:shadow-lg transition-shadow"
section="footer"
/>
<Typography className="flex-1 text-14 font-semibold mb-24 opacity-80 group-hover:opacity-100">
Footer Palette
</Typography>
</div>
}
/>
)}
/>
</div>
</div>
<Controller
name="customScrollbars"
control={control}
render={({ field: { onChange, value } }) => (
<FormControl component="fieldset" className="FuseSettings-formControl">
<FormLabel component="legend" className="text-14">
Custom Scrollbars
</FormLabel>
<Switch
checked={value}
onChange={(ev) => onChange(ev.target.checked)}
aria-label="Custom Scrollbars"
/>
</FormControl>
)}
/>
<Controller
name="direction"
control={control}
render={({ field }) => (
<FormControl component="fieldset" className="FuseSettings-formControl">
<FormLabel component="legend" className="text-14">
Direction
</FormLabel>
<RadioGroup {...field} aria-label="Layout Direction" className="FuseSettings-group" row>
<FormControlLabel key="rtl" value="rtl" control={<Radio />} label="RTL" />
<FormControlLabel key="ltr" value="ltr" control={<Radio />} label="LTR" />
</RadioGroup>
</FormControl>
)}
/>
</Root>
);
}
export default memo(FuseSettings);

View File

@@ -0,0 +1 @@
export { default } from './FuseSettings';

View File

@@ -0,0 +1,62 @@
import clsx from 'clsx';
import { Box } from '@mui/system';
function PalettePreview(props) {
const { palette, className } = props;
return (
<Box
className={clsx(
'w-200 text-left rounded-6 relative font-bold shadow overflow-hidden',
className
)}
sx={{
backgroundColor: palette.background.default,
color: palette.text.primary,
}}
type="button"
>
<Box
className="w-full h-56 px-8 pt-8 relative"
sx={{
backgroundColor: palette.primary.main,
color: (theme) =>
palette.primary.contrastText || theme.palette.getContrastText(palette.primary.main),
}}
>
<span className="text-12">Header (Primary)</span>
<Box
className="flex items-center justify-center w-20 h-20 rounded-full absolute bottom-0 right-0 -mb-10 shadow text-10 mr-4"
sx={{
backgroundColor: palette.secondary.main,
color: (theme) =>
palette.secondary.contrastText ||
theme.palette.getContrastText(palette.secondary.main),
}}
>
<span className="">S</span>
</Box>
</Box>
<div className="pl-8 pr-28 -mt-24 w-full">
<Box
className="w-full h-96 rounded-4 relative shadow p-8"
sx={{
backgroundColor: palette.background.paper,
color: palette.text.primary,
}}
>
<span className="text-12 opacity-75">Paper</span>
</Box>
</div>
<div className="px-8 py-8 w-full">
<span className="text-12 opacity-75">Background</span>
</div>
{/* <pre className="language-js p-24 w-400">{JSON.stringify(palette, null, 2)}</pre> */}
</Box>
);
}
export default PalettePreview;

View File

@@ -0,0 +1,282 @@
import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import _ from '@lodash';
import { darkPaletteText, lightPaletteText } from 'app/configs/themesConfig';
import { darken, getContrastRatio, lighten } from '@mui/material/styles';
import { useTheme } from '@mui/styles';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import { Dialog, DialogActions, DialogContent, Icon, TextField } from '@mui/material';
import Typography from '@mui/material/Typography';
import ButtonGroup from '@mui/material/ButtonGroup';
import Button from '@mui/material/Button';
import SectionPreview from './SectionPreview';
import PalettePreview from './PalettePreview';
function isDark(color) {
return getContrastRatio(color, '#ffffff') >= 3;
}
function PaletteSelector(props) {
const { value } = props;
const [openDialog, setOpenDialog] = useState(false);
const theme = useTheme();
const methods = useForm({
defaultValues: {},
mode: 'onChange',
});
const { reset, formState, trigger, handleSubmit, watch, control, setValue } = methods;
const { isValid, dirtyFields, errors } = formState;
useEffect(() => {
reset(value);
}, [value, reset]);
const form = watch();
const formType = watch('palette.mode');
useEffect(() => {
// console.info(form);
}, [form]);
useEffect(() => {
if (!formType || !openDialog) {
return;
}
setTimeout(() => trigger(['palette.background.paper', 'palette.background.default']));
}, [formType, openDialog, trigger]);
const backgroundColorValidation = (v) => {
if (formType === 'light' && isDark(v)) {
return 'Must be a light color';
}
if (formType === 'dark' && !isDark(v)) {
return 'Must be a dark color';
}
return true;
};
/**
* Open Dialog
*/
function handleOpenDialog(ev) {
ev.preventDefault();
ev.stopPropagation();
setOpenDialog(true);
}
/**
* Close Dialog
*/
function handleCloseDialog() {
setOpenDialog(false);
}
function onSubmit(formData) {
props.onChange(formData);
handleCloseDialog();
}
return (
<>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/interactive-supports-focus */}
<div onClick={handleOpenDialog} role="button">
{props.trigger}
</div>
<Dialog
container={document.body}
open={openDialog}
onClose={handleCloseDialog}
aria-labelledby="form-dialog-title"
classes={{
paper: 'rounded-5 w-full',
}}
>
<AppBar elevation={0} position="static">
<Toolbar className="flex w-full">
<Icon className="mr-12">palette</Icon>
<Typography variant="subtitle1" color="inherit">
Edit palette
</Typography>
</Toolbar>
</AppBar>
<DialogContent>
<div className="flex w-full">
<div className="flex flex-col items-center justify-center p-24 flex-1">
<Controller
name="palette.mode"
control={control}
render={({ field: { onChange: _onChange, value: _value } }) => (
<ButtonGroup
disableElevation
variant="contained"
color="secondary"
className="mb-32"
>
<Button
onClick={async () => {
_onChange('light');
setValue('palette.text', lightPaletteText, { shouldDirty: true });
}}
variant={_value === 'light' ? 'contained' : 'outlined'}
>
Light
</Button>
<Button
onClick={async () => {
_onChange('dark');
setValue('palette.text', darkPaletteText, { shouldDirty: true });
}}
variant={_value === 'dark' ? 'contained' : 'outlined'}
>
Dark
</Button>
</ButtonGroup>
)}
/>
<Controller
name="palette.primary.main"
control={control}
render={({ field: { onChange: _onChange, value: _value } }) => (
<TextField
value={_value}
onChange={(ev) => {
_onChange(ev.target.value);
setValue('palette.primary.light', lighten(ev.target.value, 0.8), {
shouldDirty: true,
});
setValue('palette.primary.dark', darken(ev.target.value, 0.2), {
shouldDirty: true,
});
setValue(
'palette.primary.contrastText',
theme.palette.getContrastText(ev.target.value),
{ shouldDirty: true }
);
}}
type="color"
variant="outlined"
className="mb-32"
label="Primary color"
InputProps={{ className: 'w-200 h-32' }}
/>
)}
/>
<Controller
name="palette.secondary.main"
control={control}
render={({ field: { onChange: _onChange, value: _value } }) => (
<TextField
value={_value}
onChange={(ev) => {
_onChange(ev.target.value);
setValue('palette.secondary.light', lighten(ev.target.value, 0.8), {
shouldDirty: true,
});
setValue('palette.secondary.dark', darken(ev.target.value, 0.2), {
shouldDirty: true,
});
setValue(
'palette.secondary.contrastText',
theme.palette.getContrastText(ev.target.value),
{ shouldDirty: true }
);
}}
type="color"
variant="outlined"
className="mb-32"
label="Secondary color"
InputProps={{ className: 'w-200 h-32' }}
/>
)}
/>
<Controller
name="palette.background.paper"
control={control}
rules={{
validate: {
backgroundColorValidation,
},
}}
render={({ field }) => (
<TextField
{...field}
type="color"
variant="outlined"
className="mb-32"
label="Background paper"
InputProps={{ className: 'w-200 h-32' }}
error={!!errors?.palette?.background?.paper}
helperText={errors?.palette?.background?.paper?.message}
/>
)}
/>
<Controller
name="palette.background.default"
control={control}
rules={{
validate: {
backgroundColorValidation,
},
}}
render={({ field }) => (
<TextField
{...field}
type="color"
variant="outlined"
className=""
label="Background default"
InputProps={{ className: 'w-200 h-32' }}
error={!!errors?.palette?.background?.default}
helperText={errors?.palette?.background?.default?.message}
/>
)}
/>
</div>
<div className="flex flex-col items-center justify-center p-48">
<Typography className="text-16 font-semibold mb-16 -mt-48" color="text.secondary">
Preview
</Typography>
<PalettePreview className="" palette={form.palette} />
</div>
</div>
</DialogContent>
<DialogActions className="flex justify-between p-16">
<Button onClick={handleCloseDialog} color="primary">
Cancel
</Button>
<Button
variant="contained"
color="secondary"
autoFocus
onClick={handleSubmit(onSubmit)}
disabled={_.isEmpty(dirtyFields) || !isValid}
>
Save
</Button>
</DialogActions>
</Dialog>
</>
);
}
PaletteSelector.defaultProps = {
trigger: (
<div className="flex flex-col items-center space-y-8 w-128 m-8">
<SectionPreview section="" />
<Typography className="flex-1 text-16 font-bold mb-24">Edit Palette</Typography>
</div>
),
};
export default PaletteSelector;

View File

@@ -0,0 +1,110 @@
import clsx from 'clsx';
import Box from '@mui/material/Box';
import { darken, lighten } from '@mui/material/styles';
import { red } from '@mui/material/colors';
function SectionPreview(props) {
const { section, className } = props;
return (
<div
className={clsx(
'flex w-128 h-80 rounded-md overflow-hidden border-1 hover:opacity-80',
className
)}
>
<Box
sx={{
backgroundColor:
section === 'navbar'
? red['100']
: (theme) =>
theme.palette.mode === 'light'
? lighten(theme.palette.background.default, 0.4)
: lighten(theme.palette.background.default, 0.02),
'& > div': {
backgroundColor:
section === 'navbar'
? red['200']
: (theme) =>
theme.palette.mode === 'light'
? darken(theme.palette.background.default, 0.1)
: lighten(theme.palette.background.default, 0.1),
},
}}
className="w-32 pt-12 px-6 space-y-1"
>
<div className="h-4 rounded-sm" />
<div className="h-4 rounded-sm" />
<div className="h-4 rounded-sm" />
<div className="h-4 rounded-sm" />
<div className="h-4 rounded-sm" />
</Box>
<div className="flex flex-col flex-auto border-l">
<Box
sx={{
backgroundColor:
section === 'toolbar'
? red['100']
: (theme) =>
theme.palette.mode === 'light'
? lighten(theme.palette.background.default, 0.4)
: lighten(theme.palette.background.default, 0.02),
'& > div': {
backgroundColor:
section === 'toolbar'
? red['200']
: (theme) =>
theme.palette.mode === 'light'
? darken(theme.palette.background.default, 0.1)
: lighten(theme.palette.background.default, 0.1),
},
}}
className={clsx('h-12 flex items-center justify-end h-full pr-6')}
>
<div className="w-4 h-4 ml-4 rounded-full" />
<div className="w-4 h-4 ml-4 rounded-full" />
<div className="w-4 h-4 ml-4 rounded-full" />
</Box>
<Box
sx={{
backgroundColor:
section === 'main'
? red['100']
: (theme) =>
theme.palette.mode === 'light'
? lighten(theme.palette.background.default, 0.4)
: lighten(theme.palette.background.default, 0.02),
}}
className={clsx('flex flex-auto border-t border-b')}
/>
<Box
sx={{
backgroundColor:
section === 'footer'
? red['100']
: (theme) =>
theme.palette.mode === 'light'
? lighten(theme.palette.background.default, 0.4)
: lighten(theme.palette.background.default, 0.02),
'& > div': {
backgroundColor:
section === 'footer'
? red['200']
: (theme) =>
theme.palette.mode === 'light'
? darken(theme.palette.background.default, 0.1)
: lighten(theme.palette.background.default, 0.1),
},
}}
className={clsx('h-12 flex items-center pr-6')}
>
<div className="w-4 h-4 ml-4 rounded-full" />
<div className="w-4 h-4 ml-4 rounded-full" />
<div className="w-4 h-4 ml-4 rounded-full" />
</Box>
</div>
</div>
);
}
export default SectionPreview;

View File

@@ -0,0 +1,234 @@
import { amber } from '@mui/material/colors';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import Input from '@mui/material/Input';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import clsx from 'clsx';
import { motion } from 'framer-motion';
import { memo, useMemo, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import FuseSvgIcon from '../FuseSvgIcon';
function FuseShortcuts(props) {
const { navigation, shortcuts, onChange } = props;
const searchInputRef = useRef(null);
const [addMenu, setAddMenu] = useState(null);
const [searchText, setSearchText] = useState('');
const [searchResults, setSearchResults] = useState(null);
const shortcutItems = shortcuts
? shortcuts.map((id) => navigation.find((item) => item.id === id))
: [];
function addMenuClick(event) {
setAddMenu(event.currentTarget);
}
function addMenuClose() {
setAddMenu(null);
}
function search(ev) {
const newSearchText = ev.target.value;
setSearchText(newSearchText);
if (newSearchText.length !== 0 && navigation) {
setSearchResults(
navigation.filter((item) => item.title.toLowerCase().includes(newSearchText.toLowerCase()))
);
return;
}
setSearchResults(null);
}
function toggleInShortcuts(id) {
let newShortcuts = [...shortcuts];
newShortcuts = newShortcuts.includes(id)
? newShortcuts.filter((_id) => id !== _id)
: [...newShortcuts, id];
onChange(newShortcuts);
}
function ShortcutMenuItem({ item, onToggle }) {
if (!item || !item.id) {
return null;
}
return (
<Link to={item.url || ''} role="button">
<MenuItem key={item.id}>
<ListItemIcon className="min-w-40">
{item.icon ? (
<FuseSvgIcon>{item.icon}</FuseSvgIcon>
) : (
<span className="text-20 font-semibold uppercase text-center">{item.title[0]}</span>
)}
</ListItemIcon>
<ListItemText primary={item.title} />
<IconButton
onClick={(ev) => {
ev.preventDefault();
ev.stopPropagation();
onToggle(item.id);
}}
size="large"
>
<FuseSvgIcon color="action">
{shortcuts.includes(item.id) ? 'heroicons-solid:star' : 'heroicons-outline:star'}
</FuseSvgIcon>
</IconButton>
</MenuItem>
</Link>
);
}
return (
<div
className={clsx(
'flex flex-1',
props.variant === 'vertical' && 'flex-col grow-0 shrink',
props.className
)}
>
{useMemo(() => {
const container = {
show: {
transition: {
staggerChildren: 0.1,
},
},
};
const item = {
hidden: { opacity: 0, scale: 0.6 },
show: { opacity: 1, scale: 1 },
};
return (
<motion.div
variants={container}
initial="hidden"
animate="show"
className={clsx('flex flex-1', props.variant === 'vertical' && 'flex-col')}
>
{shortcutItems.map(
(_item) =>
_item && (
<Link to={_item.url} key={_item.id} role="button">
<Tooltip
title={_item.title}
placement={props.variant === 'horizontal' ? 'bottom' : 'left'}
>
<IconButton
className="w-40 h-40 p-0"
component={motion.div}
variants={item}
size="large"
>
{_item.icon ? (
<FuseSvgIcon>{_item.icon}</FuseSvgIcon>
) : (
<span className="text-20 font-semibold uppercase">{_item.title[0]}</span>
)}
</IconButton>
</Tooltip>
</Link>
)
)}
<Tooltip
title="Click to add/remove shortcut"
placement={props.variant === 'horizontal' ? 'bottom' : 'left'}
>
<IconButton
component={motion.div}
variants={item}
className="w-40 h-40 p-0"
aria-owns={addMenu ? 'add-menu' : null}
aria-haspopup="true"
onClick={addMenuClick}
size="large"
>
<FuseSvgIcon sx={{ color: amber[600] }}>heroicons-solid:star</FuseSvgIcon>
</IconButton>
</Tooltip>
</motion.div>
);
}, [addMenu, props.variant, shortcutItems])}
<Menu
id="add-menu"
anchorEl={addMenu}
open={Boolean(addMenu)}
onClose={addMenuClose}
classes={{
paper: 'min-w-256',
}}
TransitionProps={{
onEntered: () => {
searchInputRef.current.focus();
},
onExited: () => {
setSearchText('');
},
}}
>
<div className="p-16 pt-8">
<Input
inputRef={searchInputRef}
value={searchText}
onChange={search}
placeholder="Search for an app or page"
className=""
fullWidth
inputProps={{
'aria-label': 'Search',
}}
disableUnderline
/>
</div>
<Divider />
{searchText.length !== 0 &&
searchResults &&
searchResults.map((_item) => (
<ShortcutMenuItem
key={_item.id}
item={_item}
onToggle={() => toggleInShortcuts(_item.id)}
/>
))}
{searchText.length !== 0 && searchResults.length === 0 && (
<Typography color="text.secondary" className="p-16 pb-8">
No results..
</Typography>
)}
{searchText.length === 0 &&
shortcutItems.map(
(_item) =>
_item && (
<ShortcutMenuItem
key={_item.id}
item={_item}
onToggle={() => toggleInShortcuts(_item.id)}
/>
)
)}
</Menu>
</div>
);
}
FuseShortcuts.propTypes = {};
FuseShortcuts.defaultProps = {
variant: 'horizontal',
};
export default memo(FuseShortcuts);

View File

@@ -0,0 +1 @@
export { default } from './FuseShortcuts';

View File

@@ -0,0 +1,261 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled } from '@mui/material/styles';
import Fab from '@mui/material/Fab';
import Hidden from '@mui/material/Hidden';
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import Tooltip from '@mui/material/Tooltip';
import clsx from 'clsx';
import { memo, useState } from 'react';
import FuseSvgIcon from '../FuseSvgIcon';
const Root = styled('div')(({ theme }) => ({
'& .FuseSidePanel-paper': {
display: 'flex',
width: 56,
transition: theme.transitions.create(['transform', 'width', 'min-width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.shorter,
}),
paddingBottom: 64,
height: '100%',
maxHeight: '100vh',
position: 'sticky',
top: 0,
zIndex: 999,
'&.left': {
'& .FuseSidePanel-buttonWrapper': {
left: 0,
right: 'auto',
},
'& .FuseSidePanel-buttonIcon': {
transform: 'rotate(0deg)',
},
},
'&.right': {
'& .FuseSidePanel-buttonWrapper': {
right: 0,
left: 'auto',
},
'& .FuseSidePanel-buttonIcon': {
transform: 'rotate(-180deg)',
},
},
'&.closed': {
[theme.breakpoints.up('lg')]: {
width: 0,
},
'&.left': {
'& .FuseSidePanel-buttonWrapper': {
justifyContent: 'start',
},
'& .FuseSidePanel-button': {
borderBottomLeftRadius: 0,
borderTopLeftRadius: 0,
paddingLeft: 4,
},
'& .FuseSidePanel-buttonIcon': {
transform: 'rotate(-180deg)',
},
},
'&.right': {
'& .FuseSidePanel-buttonWrapper': {
justifyContent: 'flex-end',
},
'& .FuseSidePanel-button': {
borderBottomRightRadius: 0,
borderTopRightRadius: 0,
paddingRight: 4,
},
'& .FuseSidePanel-buttonIcon': {
transform: 'rotate(0deg)',
},
},
'& .FuseSidePanel-buttonWrapper': {
width: 'auto',
},
'& .FuseSidePanel-button': {
backgroundColor: theme.palette.background.paper,
borderRadius: 38,
transition: theme.transitions.create(
['background-color', 'border-radius', 'width', 'min-width', 'padding'],
{
easing: theme.transitions.easing.easeInOut,
duration: theme.transitions.duration.shorter,
}
),
width: 24,
'&:hover': {
width: 52,
paddingLeft: 8,
paddingRight: 8,
},
},
'& .FuseSidePanel-content': {
opacity: 0,
},
},
},
'& .FuseSidePanel-content': {
overflow: 'hidden',
opacity: 1,
transition: theme.transitions.create(['opacity'], {
easing: theme.transitions.easing.easeInOut,
duration: theme.transitions.duration.short,
}),
},
'& .FuseSidePanel-buttonWrapper': {
position: 'absolute',
bottom: 0,
left: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '12px 0',
width: '100%',
minWidth: 56,
},
'& .FuseSidePanel-button': {
padding: 8,
width: 40,
height: 40,
},
'& .FuseSidePanel-buttonIcon': {
transition: theme.transitions.create(['transform'], {
easing: theme.transitions.easing.easeInOut,
duration: theme.transitions.duration.short,
}),
},
'& .FuseSidePanel-mobileButton': {
height: 40,
position: 'absolute',
zIndex: 99,
bottom: 12,
width: 24,
borderRadius: 38,
padding: 8,
backgroundColor: theme.palette.background.paper,
transition: theme.transitions.create(
['background-color', 'border-radius', 'width', 'min-width', 'padding'],
{
easing: theme.transitions.easing.easeInOut,
duration: theme.transitions.duration.shorter,
}
),
'&:hover': {
width: 52,
paddingLeft: 8,
paddingRight: 8,
},
'&.left': {
borderBottomLeftRadius: 0,
borderTopLeftRadius: 0,
paddingLeft: 4,
left: 0,
},
'&.right': {
borderBottomRightRadius: 0,
borderTopRightRadius: 0,
paddingRight: 4,
right: 0,
'& .FuseSidePanel-buttonIcon': {
transform: 'rotate(-180deg)',
},
},
},
}));
function FuseSidePanel(props) {
const [opened, setOpened] = useState(props.opened);
const [mobileOpen, setMobileOpen] = useState(false);
function toggleOpened() {
setOpened(!opened);
}
function toggleMobileDrawer() {
setMobileOpen(!mobileOpen);
}
return (
<Root>
<Hidden lgDown>
<Paper
className={clsx(
'FuseSidePanel-paper',
props.className,
opened ? 'opened' : 'closed',
props.position,
'shadow-lg'
)}
square
>
<FuseScrollbars className={clsx('content', 'FuseSidePanel-content')}>
{props.children}
</FuseScrollbars>
<div className="FuseSidePanel-buttonWrapper">
<Tooltip
title="Toggle side panel"
placement={props.position === 'left' ? 'right' : 'right'}
>
<IconButton
className="FuseSidePanel-button"
onClick={toggleOpened}
disableRipple
size="large"
>
<FuseSvgIcon className="FuseSidePanel-buttonIcon">
heroicons-outline:chevron-left
</FuseSvgIcon>
</IconButton>
</Tooltip>
</div>
</Paper>
</Hidden>
<Hidden lgUp>
<SwipeableDrawer
classes={{
paper: clsx('FuseSidePanel-paper', props.className),
}}
anchor={props.position}
open={mobileOpen}
onOpen={(ev) => {}}
onClose={toggleMobileDrawer}
disableSwipeToOpen
>
<FuseScrollbars className={clsx('content', 'FuseSidePanel-content')}>
{props.children}
</FuseScrollbars>
</SwipeableDrawer>
<Tooltip title="Hide side panel" placement={props.position === 'left' ? 'right' : 'right'}>
<Fab
className={clsx('FuseSidePanel-mobileButton', props.position)}
onClick={toggleMobileDrawer}
disableRipple
>
<FuseSvgIcon className="FuseSidePanel-buttonIcon">
heroicons-outline:chevron-right
</FuseSvgIcon>
</Fab>
</Tooltip>
</Hidden>
</Root>
);
}
FuseSidePanel.propTypes = {};
FuseSidePanel.defaultProps = {
position: 'left',
opened: true,
};
export default memo(FuseSidePanel);

View File

@@ -0,0 +1 @@
export { default } from './FuseSidePanel';

View File

@@ -0,0 +1,26 @@
import { memo } from 'react';
import Box from '@mui/material/Box';
function FuseSplashScreen() {
return (
<div id="fuse-splash-screen">
<div className="logo">
<img width="128" src="assets/images/logo/logo.svg" alt="logo" />
</div>
<Box
id="spinner"
sx={{
'& > div': {
backgroundColor: 'palette.secondary.main',
},
}}
>
<div className="bounce1" />
<div className="bounce2" />
<div className="bounce3" />
</Box>
</div>
);
}
export default memo(FuseSplashScreen);

View File

@@ -0,0 +1 @@
export { default } from './FuseSplashScreen';

View File

@@ -0,0 +1,22 @@
import FuseLoading from '@fuse/core/FuseLoading';
import PropTypes from 'prop-types';
import { Suspense } from 'react';
/**
* React Suspense defaults
* For to Avoid Repetition
*/ function FuseSuspense(props) {
return <Suspense fallback={<FuseLoading {...props.loadingProps} />}>{props.children}</Suspense>;
}
FuseSuspense.propTypes = {
loadingProps: PropTypes.object,
};
FuseSuspense.defaultProps = {
loadingProps: {
delay: 0,
},
};
export default FuseSuspense;

View File

@@ -0,0 +1 @@
export { default } from './FuseSuspense';

View File

@@ -0,0 +1,83 @@
import clsx from 'clsx';
import { forwardRef, useMemo } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
import { Box } from '@mui/system';
import Icon from '@mui/material/Icon';
const Root = styled(Box)(({ theme, ...props }) => ({
width: props.size,
height: props.size,
minWidth: props.size,
minHeight: props.size,
fontSize: props.size,
lineHeight: props.size,
color: {
primary: theme.palette.primary.main,
secondary: theme.palette.secondary.main,
info: theme.palette.info.main,
success: theme.palette.success.main,
warning: theme.palette.warning.main,
action: theme.palette.action.active,
error: theme.palette.error.main,
disabled: theme.palette.action.disabled,
inherit: undefined,
}[props.color],
}));
const FuseSvgIcon = forwardRef((props, ref) => {
const { children, size, sx, className, color } = props;
const iconPath = props.children.replace(':', '.svg#');
return useMemo(
() => (
<>
{!props.children.includes(':') ? (
<Icon ref={ref} {...props} />
) : (
<Root
{...props}
component="svg"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
className={clsx('shrink-0 fill-current ', className)}
ref={ref}
size={size}
sx={sx}
color={color}
>
<use xlinkHref={`assets/icons/${iconPath}`} />
</Root>
)}
</>
),
[children, ref, className, size, sx, color, iconPath]
);
});
FuseSvgIcon.propTypes = {
children: PropTypes.string,
size: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
sx: PropTypes.object,
color: PropTypes.oneOf([
'inherit',
'disabled',
'primary',
'secondary',
'action',
'error',
'info',
'success',
'warning',
]),
};
FuseSvgIcon.defaultProps = {
children: '',
size: 24,
sx: {},
color: 'inherit',
};
export default FuseSvgIcon;

View File

@@ -0,0 +1 @@
export { default } from './FuseSvgIcon';

View File

@@ -0,0 +1,23 @@
import { ThemeProvider } from '@mui/material/styles';
import { memo, useEffect, useLayoutEffect } from 'react';
const useEnhancedEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
function FuseTheme(props) {
const { direction, theme } = props;
const { mode } = theme.palette;
useEnhancedEffect(() => {
document.body.dir = direction;
}, [direction]);
useEffect(() => {
document.body.classList.add(mode === 'light' ? 'light' : 'dark');
document.body.classList.remove(mode === 'light' ? 'dark' : 'light');
}, [mode]);
// console.warn('FuseTheme:: rendered',mainTheme);
return <ThemeProvider theme={theme}>{props.children}</ThemeProvider>;
}
export default memo(FuseTheme);

View File

@@ -0,0 +1 @@
export { default } from './FuseTheme';

View File

@@ -0,0 +1,94 @@
import Typography from '@mui/material/Typography';
import { useTheme } from '@mui/material/styles';
import { memo } from 'react';
import clsx from 'clsx';
function SchemePreview({ theme, className, id, onSelect }) {
const _theme = useTheme();
const primaryColor = theme.palette.primary[500]
? theme.palette.primary[500]
: theme.palette.primary.main;
const primaryColorContrast =
theme.palette.primary.contrastText || _theme.palette.getContrastText(primaryColor);
const secondaryColor = theme.palette.secondary[500]
? theme.palette.secondary[500]
: theme.palette.secondary.main;
const secondaryColorContrast =
theme.palette.secondary.contrastText || _theme.palette.getContrastText(secondaryColor);
const backgroundColor = theme.palette.background.default;
const backgroundColorContrast = _theme.palette.getContrastText(theme.palette.background.default);
const paperColor = theme.palette.background.paper;
const paperColorContrast = _theme.palette.getContrastText(theme.palette.background.paper);
return (
<div className={clsx(className, 'mb-8')}>
<button
className={clsx(
'w-full text-left rounded-6 relative font-500 shadow hover:shadow-md transition-shadow cursor-pointer overflow-hidden'
)}
style={{
backgroundColor,
color: backgroundColorContrast,
}}
onClick={() => onSelect(theme)}
type="button"
>
<div
className="w-full h-56 px-8 pt-8 relative"
style={{
backgroundColor: primaryColor,
color: primaryColorContrast,
}}
>
<span className="text-12 opacity-75">Header (Primary)</span>
<div
className="flex items-center justify-center w-20 h-20 rounded-full absolute bottom-0 right-0 -mb-10 shadow text-10 mr-4"
style={{
backgroundColor: secondaryColor,
color: secondaryColorContrast,
}}
>
<span className="opacity-75">S</span>
</div>
</div>
<div className="pl-8 pr-28 -mt-24 w-full">
<div
className="w-full h-96 rounded-4 relative shadow p-8"
style={{
backgroundColor: paperColor,
color: paperColorContrast,
}}
>
<span className="text-12 opacity-75">Paper</span>
</div>
</div>
<div className="px-8 py-8 w-full">
<span className="text-12 opacity-75">Background</span>
</div>
</button>
<Typography className="font-semibold w-full text-center mt-12">{id}</Typography>
</div>
);
}
function FuseThemeSchemes(props) {
const { themes } = props;
return (
<div>
<div className="flex flex-wrap w-full -mx-8">
{Object.entries(themes)
.filter(([key, val]) => !(key === 'mainThemeDark' || key === 'mainThemeLight'))
.map(([key, val]) => (
<div key={key} className="w-1/2 p-8">
<SchemePreview id={key} theme={val} onSelect={() => props?.onSelect(val)} />
</div>
))}
</div>
</div>
);
}
export default memo(FuseThemeSchemes);

View File

@@ -0,0 +1 @@
export { default } from './FuseThemeSchemes';

View File

@@ -0,0 +1,20 @@
import { forwardRef } from 'react';
import { NavLink as BaseNavLink } from 'react-router-dom';
const NavLinkAdapter = forwardRef(({ activeClassName, activeStyle, ...props }, ref) => {
return (
<BaseNavLink
ref={ref}
{...props}
className={({ isActive }) =>
[props.className, isActive ? activeClassName : null].filter(Boolean).join(' ')
}
style={({ isActive }) => ({
...props.style,
...(isActive ? activeStyle : null),
})}
/>
);
});
export default NavLinkAdapter;

View File

@@ -0,0 +1 @@
export { default } from './NavLinkAdapter';

View File

@@ -0,0 +1 @@
export { default } from './withRouter';

View File

@@ -0,0 +1,11 @@
import { useLocation, useNavigate } from 'react-router-dom';
function withRouter(Child) {
return (props) => {
const location = useLocation();
const navigate = useNavigate();
return <Child {...props} navigate={navigate} location={location} />;
};
}
export default withRouter;

View File

@@ -0,0 +1 @@
export { default } from './withRouterAndRef';

View File

@@ -0,0 +1,16 @@
import { Component, forwardRef } from 'react';
import withRouter from '@fuse/core/withRouter';
const withRouterAndRef = (WrappedComponent) => {
class InnerComponentWithRef extends Component {
render() {
const { forwardRef: _forwardRef, ...rest } = this.props;
return <WrappedComponent {...rest} ref={_forwardRef} />;
}
}
const ComponentWithRouter = withRouter(InnerComponentWithRef, { withRef: true });
return forwardRef((props, ref) => <ComponentWithRouter {...props} forwardRef={ref} />);
};
export default withRouterAndRef;

View File

@@ -0,0 +1,318 @@
import { fuseDark } from '@fuse/colors';
import { lightBlue, red } from '@mui/material/colors';
import { createTheme } from '@mui/material/styles';
import qs from 'qs';
const defaultTheme = {
palette: {
mode: 'light',
text: {
primary: 'rgb(17, 24, 39)',
secondary: 'rgb(107, 114, 128)',
disabled: 'rgb(149, 156, 169)',
},
common: {
black: 'rgb(17, 24, 39)',
white: 'rgb(255, 255, 255)',
},
primary: {
light: '#bec1c5',
main: '#252f3e',
dark: '#0d121b',
contrastDefaultColor: 'light',
},
secondary: {
light: '#bdf2fa',
main: '#22d3ee',
dark: '#0cb7e2',
},
background: {
paper: '#FFFFFF',
default: '#f6f7f9',
},
error: {
light: '#ffcdd2',
main: '#f44336',
dark: '#b71c1c',
},
},
};
/**
* SETTINGS DEFAULTS
*/
export const defaultSettings = {
customScrollbars: true,
direction: 'ltr',
theme: {
main: defaultTheme,
navbar: defaultTheme,
toolbar: defaultTheme,
footer: defaultTheme,
},
};
export function getParsedQuerySettings() {
const parsedQueryString = qs.parse(window.location.search, { ignoreQueryPrefix: true });
if (parsedQueryString && parsedQueryString.defaultSettings) {
return JSON.parse(parsedQueryString.defaultSettings);
}
return {};
// Generating route params from settings
/* const settings = qs.stringify({
defaultSettings: JSON.stringify(defaultSettings, {strictNullHandling: true})
});
console.info(settings); */
}
/**
* THEME DEFAULTS
*/
export const defaultThemeOptions = {
typography: {
fontFamily: ['Inter var', 'Roboto', '"Helvetica"', 'Arial', 'sans-serif'].join(','),
fontWeightLight: 300,
fontWeightRegular: 400,
fontWeightMedium: 500,
},
components: {
MuiDateTimePicker: {
defaultProps: {
PopperProps: { className: 'z-9999' },
},
},
MuiAppBar: {
defaultProps: {
enableColorOnDark: true,
},
styleOverrides: {
root: {
backgroundImage: 'none',
},
},
},
MuiButtonBase: {
defaultProps: {
disableRipple: true,
},
},
MuiButton: {
defaultProps: {
variant: 'text',
color: 'inherit',
},
styleOverrides: {
root: {
textTransform: 'none',
// lineHeight: 1,
},
sizeMedium: {
borderRadius: 20,
height: 40,
minHeight: 40,
maxHeight: 40,
},
sizeSmall: {
borderRadius: '15px',
},
sizeLarge: {
borderRadius: '28px',
},
contained: {
boxShadow: 'none',
'&:hover, &:focus': {
boxShadow: 'none',
},
},
},
},
MuiButtonGroup: {
defaultProps: {
color: 'secondary',
},
styleOverrides: {
contained: {
borderRadius: 18,
},
},
},
MuiTab: {
styleOverrides: {
root: {
textTransform: 'none',
},
},
},
MuiDialog: {
styleOverrides: {
paper: {
borderRadius: 16,
},
},
},
MuiPaper: {
styleOverrides: {
root: {
backgroundImage: 'none',
},
rounded: {
borderRadius: 16,
},
},
},
MuiPopover: {
styleOverrides: {
paper: {
borderRadius: 8,
},
},
},
MuiTextField: {
defaultProps: {
color: 'secondary',
},
},
MuiInputLabel: {
defaultProps: {
color: 'secondary',
},
},
MuiSelect: {
defaultProps: {
color: 'secondary',
},
},
MuiOutlinedInput: {
defaultProps: {
color: 'secondary',
},
},
MuiInputBase: {
styleOverrides: {
root: {
minHeight: 40,
lineHeight: 1,
},
},
},
MuiFilledInput: {
styleOverrides: {
root: {
borderRadius: 4,
'&:before, &:after': {
display: 'none',
},
},
},
},
MuiSlider: {
defaultProps: {
color: 'secondary',
},
},
MuiCheckbox: {
defaultProps: {
color: 'secondary',
},
},
MuiRadio: {
defaultProps: {
color: 'secondary',
},
},
MuiSwitch: {
defaultProps: {
color: 'secondary',
},
},
MuiTypography: {
variants: [
{
props: { color: 'text.secondary' },
style: {
color: 'text.secondary',
},
},
],
},
},
};
export const mustHaveThemeOptions = {
typography: {
htmlFontSize: 10,
fontSize: 14,
body1: {
fontSize: '1.4rem',
},
body2: {
fontSize: '1.4rem',
},
},
};
export const defaultThemes = {
default: {
palette: {
mode: 'light',
primary: fuseDark,
secondary: {
light: lightBlue[400],
main: lightBlue[600],
dark: lightBlue[700],
},
error: red,
},
status: {
danger: 'orange',
},
},
defaultDark: {
palette: {
mode: 'dark',
primary: fuseDark,
secondary: {
light: lightBlue[400],
main: lightBlue[600],
dark: lightBlue[700],
},
error: red,
},
status: {
danger: 'orange',
},
},
};
export function extendThemeWithMixins(obj) {
const theme = createTheme(obj);
return {
border: (width = 1) => ({
borderWidth: width,
borderStyle: 'solid',
borderColor: theme.palette.divider,
}),
borderLeft: (width = 1) => ({
borderLeftWidth: width,
borderStyle: 'solid',
borderColor: theme.palette.divider,
}),
borderRight: (width = 1) => ({
borderRightWidth: width,
borderStyle: 'solid',
borderColor: theme.palette.divider,
}),
borderTop: (width = 1) => ({
borderTopWidth: width,
borderStyle: 'solid',
borderColor: theme.palette.divider,
}),
borderBottom: (width = 1) => ({
borderBottomWidth: width,
borderStyle: 'solid',
borderColor: theme.palette.divider,
}),
};
}

View File

@@ -0,0 +1 @@
export * from './FuseDefaultSettings';

8
src/@fuse/hooks/index.js Normal file
View File

@@ -0,0 +1,8 @@
export { default as useForm } from './useForm';
export { default as useDebounce } from './useDebounce';
export { default as useTimeout } from './useTimeout';
export { default as usePrevious } from './usePrevious';
export { default as useUpdateEffect } from './useUpdateEffect';
export { default as useDeepCompareEffect } from './useDeepCompareEffect';
export { default as useThemeMediaQuery } from './useThemeMediaQuery';
export { default as useEventListener } from './useEventListener';

View File

@@ -0,0 +1,8 @@
import _ from '@lodash';
import { useRef } from 'react';
function useDebounce(func, wait, options) {
return useRef(_.debounce(func, wait, options)).current;
}
export default useDebounce;

View File

@@ -0,0 +1,47 @@
import { useEffect, useRef } from 'react';
import deepEqual from 'lodash/isEqual';
/**
* https://github.com/kentcdodds/use-deep-compare-effect
*/
function checkDeps(deps) {
if (!deps || !deps.length) {
throw new Error(
'useDeepCompareEffect should not be used with no dependencies. Use React.useEffect instead.'
);
}
if (deps.every(isPrimitive)) {
throw new Error(
'useDeepCompareEffect should not be used with dependencies that are all primitive values. Use React.useEffect instead.'
);
}
}
function isPrimitive(val) {
return val == null || /^[sbn]/.test(typeof val);
}
function useDeepCompareMemoize(value) {
const ref = useRef();
if (!deepEqual(value, ref.current)) {
ref.current = value;
}
return ref.current;
}
function useDeepCompareEffect(callback, dependencies) {
if (process.env.NODE_ENV !== 'production') {
checkDeps(dependencies);
}
// eslint-disable-next-line
useEffect(callback, useDeepCompareMemoize(dependencies));
}
export function useDeepCompareEffectNoCheck(callback, dependencies) {
// eslint-disable-next-line
useEffect(callback, useDeepCompareMemoize(dependencies));
}
export default useDeepCompareEffect;

View File

@@ -0,0 +1,42 @@
import { useEffect, useRef } from 'react';
/**
* https://usehooks.com/useEventListener/
*/
function useEventListener(eventName, handler, element = window) {
// Create a ref that stores handler
const savedHandler = useRef();
// Update ref.current value if handler changes.
// This allows our effect below to always get latest handler ...
// ... without us needing to pass it in effect deps array ...
// ... and potentially cause effect to re-run every render.
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(
() => {
// Make sure element supports addEventListener
// On
const isSupported = element && element.addEventListener;
if (!isSupported) {
return false;
}
// Create event listener that calls handler function stored in ref
const eventListener = (event) => savedHandler.current(event);
// Add event listener
element.addEventListener(eventName, eventListener);
// Remove event listener on cleanup
return () => {
element.removeEventListener(eventName, eventListener);
};
},
[eventName, element] // Re-run if eventName or element changes
);
}
export default useEventListener;

View File

@@ -0,0 +1,50 @@
import _ from '@lodash';
import { useCallback, useState } from 'react';
function useForm(initialState, onSubmit) {
const [form, setForm] = useState(initialState);
const handleChange = useCallback((event) => {
event.persist();
setForm((_form) =>
_.setIn(
{ ..._form },
event.target.name,
event.target.type === 'checkbox' ? event.target.checked : event.target.value
)
);
}, []);
const resetForm = useCallback(() => {
if (!_.isEqual(initialState, form)) {
setForm(initialState);
}
}, [form, initialState]);
const setInForm = useCallback((name, value) => {
setForm((_form) => _.setIn(_form, name, value));
}, []);
const handleSubmit = useCallback(
(event) => {
if (event) {
event.preventDefault();
}
if (onSubmit) {
onSubmit();
}
},
[onSubmit]
);
return {
form,
handleChange,
handleSubmit,
resetForm,
setForm,
setInForm,
};
}
export default useForm;

View File

@@ -0,0 +1,17 @@
import { useEffect, useRef } from 'react';
function usePrevious(value) {
// The ref object is a generic container whose current property is mutable ...
// ... and can hold any value, similar to an instance property on a class
const ref = useRef();
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}
export default usePrevious;

View File

@@ -0,0 +1,33 @@
import { useEffect, useState } from 'react';
import { useTheme } from '@mui/material/styles';
function useThemeMediaQuery(themeCallbackFunc) {
const theme = useTheme();
const query = themeCallbackFunc(theme).replace('@media ', '');
const [matches, setMatches] = useState(getMatches(query));
function getMatches(q) {
return window.matchMedia(q).matches;
}
useEffect(
() => {
const mediaQuery = window.matchMedia(query);
// Update the state with the current value
setMatches(getMatches(query));
// Create an event listener
const handler = (event) => setMatches(event.matches);
// Attach the event listener to know when the matches value changes
mediaQuery.addEventListener('change', handler);
// Remove the event listener on cleanup
return () => mediaQuery.removeEventListener('change', handler);
},
[query] // Empty array ensures effect is only run on mount and unmount
);
return matches;
}
export default useThemeMediaQuery;

View File

@@ -0,0 +1,25 @@
import { useEffect, useRef } from 'react';
function useTimeout(callback, delay) {
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
useEffect(() => {
let timer;
if (delay && callback && typeof callback === 'function') {
timer = setTimeout(callbackRef.current, delay || 0);
}
return () => {
if (timer) {
clearTimeout(timer);
}
};
}, [callback, delay]);
}
export default useTimeout;

View File

@@ -0,0 +1,16 @@
import { useEffect, useRef } from 'react';
const useUpdateEffect = (effect, deps) => {
const isInitialMount = useRef(true);
// eslint-disable-next-line
useEffect(
isInitialMount.current
? () => {
isInitialMount.current = false;
}
: effect,
deps
);
};
export default useUpdateEffect;

View File

@@ -0,0 +1,37 @@
const plugin = require('tailwindcss/plugin');
const iconSize = plugin(
({ addUtilities, theme, e, variants }) => {
const values = theme('iconSize');
addUtilities(
Object.entries(values).map(([key, value]) => ({
[`.${e(`icon-size-${key}`)}`]: {
width: value,
height: value,
minWidth: value,
minHeight: value,
fontSize: value,
lineHeight: value,
[`svg`]: {
width: value,
height: value,
},
},
})),
variants('iconSize')
);
},
{
theme: {
iconSize: (theme) => ({
...theme('spacing'),
}),
},
variants: {
iconSize: ['responsive'],
},
}
);
module.exports = iconSize;

View File

@@ -0,0 +1,391 @@
// eslint-disable-next-line max-classes-per-file
import _ from '@lodash';
import * as colors from '@mui/material/colors';
class EventEmitter {
constructor() {
this.events = {};
}
_getEventListByName(eventName) {
if (typeof this.events[eventName] === 'undefined') {
this.events[eventName] = new Set();
}
return this.events[eventName];
}
on(eventName, fn) {
this._getEventListByName(eventName).add(fn);
}
once(eventName, fn) {
const self = this;
const onceFn = (...args) => {
self.removeListener(eventName, onceFn);
fn.apply(self, args);
};
this.on(eventName, onceFn);
}
emit(eventName, ...args) {
this._getEventListByName(eventName).forEach(
// eslint-disable-next-line func-names
function (fn) {
fn.apply(this, args);
}.bind(this)
);
}
removeListener(eventName, fn) {
this._getEventListByName(eventName).delete(fn);
}
}
class FuseUtils {
static filterArrayByString(mainArr, searchText) {
if (searchText === '') {
return mainArr;
}
searchText = searchText.toLowerCase();
return mainArr.filter((itemObj) => this.searchInObj(itemObj, searchText));
}
static searchInObj(itemObj, searchText) {
if (!itemObj) {
return false;
}
const propArray = Object.keys(itemObj);
for (let i = 0; i < propArray.length; i += 1) {
const prop = propArray[i];
const value = itemObj[prop];
if (typeof value === 'string') {
if (this.searchInString(value, searchText)) {
return true;
}
} else if (Array.isArray(value)) {
if (this.searchInArray(value, searchText)) {
return true;
}
}
if (typeof value === 'object') {
if (this.searchInObj(value, searchText)) {
return true;
}
}
}
return false;
}
static searchInArray(arr, searchText) {
arr.forEach((value) => {
if (typeof value === 'string') {
if (this.searchInString(value, searchText)) {
return true;
}
}
if (typeof value === 'object') {
if (this.searchInObj(value, searchText)) {
return true;
}
}
return false;
});
return false;
}
static searchInString(value, searchText) {
return value.toLowerCase().includes(searchText);
}
static generateGUID() {
function S4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return S4() + S4();
}
static toggleInArray(item, array) {
if (array.indexOf(item) === -1) {
array.push(item);
} else {
array.splice(array.indexOf(item), 1);
}
}
static handleize(text) {
return text
.toString()
.toLowerCase()
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/\W+/g, '') // Remove all non-word chars
.replace(/--+/g, '-') // Replace multiple - with single -
.replace(/^-+/, '') // Trim - from start of text
.replace(/-+$/, ''); // Trim - from end of text
}
static setRoutes(config, defaultAuth) {
let routes = [...config.routes];
routes = routes.map((route) => {
let auth = config.auth || config.auth === null ? config.auth : defaultAuth || null;
auth = route.auth || route.auth === null ? route.auth : auth;
const settings = _.merge({}, config.settings, route.settings);
return {
...route,
settings,
auth,
};
});
return [...routes];
}
static generateRoutesFromConfigs(configs, defaultAuth) {
let allRoutes = [];
configs.forEach((config) => {
allRoutes = [...allRoutes, ...this.setRoutes(config, defaultAuth)];
});
return allRoutes;
}
static findById(obj, id) {
let i;
let childObj;
let result;
if (id === obj.id) {
return obj;
}
for (i = 0; i < Object.keys(obj).length; i += 1) {
childObj = obj[Object.keys(obj)[i]];
if (typeof childObj === 'object') {
result = this.findById(childObj, id);
if (result) {
return result;
}
}
}
return false;
}
static getFlatNavigation(navigationItems, flatNavigation = []) {
for (let i = 0; i < navigationItems.length; i += 1) {
const navItem = navigationItems[i];
if (navItem.type === 'item') {
flatNavigation.push({
id: navItem.id,
title: navItem.title,
type: navItem.type,
icon: navItem.icon || false,
url: navItem.url,
auth: navItem.auth || null,
});
}
if (navItem.type === 'collapse' || navItem.type === 'group') {
if (navItem.children) {
this.getFlatNavigation(navItem.children, flatNavigation);
}
}
}
return flatNavigation;
}
static randomMatColor(hue) {
hue = hue || '400';
const mainColors = [
'red',
'pink',
'purple',
'deepPurple',
'indigo',
'blue',
'lightBlue',
'cyan',
'teal',
'green',
'lightGreen',
'lime',
'yellow',
'amber',
'orange',
'deepOrange',
];
const randomColor = mainColors[Math.floor(Math.random() * mainColors.length)];
return colors[randomColor][hue];
}
static difference(object, base) {
function changes(_object, _base) {
return _.transform(_object, (result, value, key) => {
if (!_.isEqual(value, _base[key])) {
result[key] =
_.isObject(value) && _.isObject(_base[key]) ? changes(value, _base[key]) : value;
}
});
}
return changes(object, base);
}
static EventEmitter = EventEmitter;
static updateNavItem(nav, id, item) {
return nav.map((_item) => {
if (_item.id === id) {
return _.merge({}, _item, item);
}
if (_item.children) {
return _.merge({}, _item, {
children: this.updateNavItem(_item.children, id, item),
});
}
return _.merge({}, _item);
});
}
static removeNavItem(nav, id) {
return nav
.map((_item) => {
if (_item.id === id) {
return null;
}
if (_item.children) {
return _.merge({}, _.omit(_item, ['children']), {
children: this.removeNavItem(_item.children, id),
});
}
return _.merge({}, _item);
})
.filter((s) => s);
}
static prependNavItem(nav, item, parentId) {
if (!parentId) {
return [item, ...nav];
}
return nav.map((_item) => {
if (_item.id === parentId && _item.children) {
return {
..._item,
children: [item, ..._item.children],
};
}
if (_item.children) {
return _.merge({}, _item, {
children: this.prependNavItem(_item.children, item, parentId),
});
}
return _.merge({}, _item);
});
}
static appendNavItem(nav, item, parentId) {
if (!parentId) {
return [...nav, item];
}
return nav.map((_item) => {
if (_item.id === parentId && _item.children) {
return {
..._item,
children: [..._item.children, item],
};
}
if (_item.children) {
return _.merge({}, _item, {
children: this.appendNavItem(_item.children, item, parentId),
});
}
return _.merge({}, _item);
});
}
static hasPermission(authArr, userRole) {
/**
* If auth array is not defined
* Pass and allow
*/
if (authArr === null || authArr === undefined) {
// console.info("auth is null || undefined:", authArr);
return true;
}
if (authArr.length === 0) {
/**
* if auth array is empty means,
* allow only user role is guest (null or empty[])
*/
// console.info("auth is empty[]:", authArr);
return !userRole || userRole.length === 0;
}
/**
* Check if user has grants
*/
// console.info("auth arr:", authArr);
/*
Check if user role is array,
*/
if (userRole && Array.isArray(userRole)) {
return authArr.some((r) => userRole.indexOf(r) >= 0);
}
/*
Check if user role is string,
*/
return authArr.includes(userRole);
}
static filterRecursive(data, predicate) {
// if no data is sent in, return null, otherwise transform the data
return !data
? null
: data.reduce((list, entry) => {
let clone = null;
if (predicate(entry)) {
// if the object matches the filter, clone it as it is
clone = { ...entry };
}
if (entry.children != null) {
// if the object has childrens, filter the list of children
const children = this.filterRecursive(entry.children, predicate);
if (children.length > 0) {
// if any of the children matches, clone the parent object, overwrite
// the children list with the filtered list
clone = { ...entry, children };
}
}
// if there's a cloned object, push it to the output list
if (clone) {
list.push(clone);
}
return list;
}, []);
}
}
export default FuseUtils;

1
src/@fuse/utils/index.js Normal file
View File

@@ -0,0 +1 @@
export { default } from './FuseUtils';

3
src/@history/@history.js Normal file
View File

@@ -0,0 +1,3 @@
import * as history from 'history';
export default history.createBrowserHistory();

1
src/@history/index.js Normal file
View File

@@ -0,0 +1 @@
export { default } from './@history';

17
src/@lodash/@lodash.js Normal file
View File

@@ -0,0 +1,17 @@
import __ from 'lodash';
/**
* You can extend Lodash with mixins
* And use it as below
* import _ from '@lodash'
*/
const _ = __.runInContext();
_.mixin({
// Immutable Set for setting state
setIn: (state, name, value) => {
return _.setWith(_.clone(state), name, value, _.clone);
},
});
export default _;

1
src/@lodash/index.js Normal file
View File

@@ -0,0 +1 @@
export { default } from './@lodash';

Some files were not shown because too many files have changed in this diff Show More