├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .storybook ├── addons.js ├── config.js ├── global.css └── preview-head.html ├── README.md ├── jsconfig.json ├── package.json ├── src ├── components │ ├── Content.js │ ├── Footer.js │ ├── Header.js │ ├── Nav.js │ └── Root.js ├── context │ ├── LayoutContext.js │ └── index.js ├── hooks │ ├── useAutoCollapse.js │ ├── useConfig.js │ ├── useEventListener.js │ ├── useHeightAdjustment.js │ ├── useMergedConfig.js │ └── useWidth.js ├── index.js └── utils │ ├── createGetScreenValue.js │ ├── get.js │ ├── getWindowSizes.js │ ├── isPlainObject.js │ ├── presets.js │ └── someIs.js ├── stories ├── 0welcome.stories.js ├── components.stories.js ├── customStyles.stories.js ├── customStyles │ ├── CustomHeader.js │ ├── CustomNav.js │ ├── customHeader.md │ └── customNav.md ├── examples.stories.js ├── mock │ ├── ContentEx.js │ ├── FooterEx.js │ ├── HeaderEx.js │ ├── NavContentEx.js │ ├── NavHeaderEx.js │ ├── RouterNavEx.js │ └── SerratedTabs.js └── presets.stories.js ├── yarn-error.log └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/react", "@babel/env"], 3 | "plugins": [ 4 | [ 5 | "module-resolver", 6 | { 7 | "root": ["./src"] 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /stories/* 2 | /dist/* -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier", "prettier/react"], 3 | "plugins": ["react", "prettier"], 4 | "rules": { 5 | "react/jsx-filename-extension": [ 6 | 1, 7 | { 8 | "extensions": [".js", "jsx"] 9 | } 10 | ], 11 | "prettier/prettier": "error", 12 | "max-len": ["error", 80], 13 | "import/no-unresolved": "off", 14 | "react/jsx-curly-brace-presence": "off", 15 | "import/no-extraneous-dependencies": "off", 16 | "import/prefer-default-export": "off" 17 | }, 18 | "parser": "babel-eslint", 19 | "env": { 20 | "browser": true, 21 | "es6": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .idea 4 | 5 | *.tgz -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | tests/ 3 | .babelrc 4 | !dist/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | public/* 3 | dist/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-notes/register'; 2 | import '@storybook/addon-actions/register'; 3 | import '@storybook/addon-links/register'; 4 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { configure, addDecorator } from '@storybook/react'; 3 | import CssBaseline from '@material-ui/core/CssBaseline'; 4 | import { createMuiTheme } from '@material-ui/core/styles'; 5 | import { ThemeProvider } from '@material-ui/styles'; 6 | import './global.css'; 7 | 8 | // automatically import all files ending in *.stories.js 9 | const req = require.context('../stories', true, /\.stories\.js$/); 10 | 11 | const baseTheme = createMuiTheme(); 12 | const ThemeDecorator = storyFn => ( 13 | <> 14 | 15 | 16 | {storyFn()} 17 | 18 | 19 | ); 20 | 21 | function loadStories() { 22 | addDecorator(ThemeDecorator); 23 | req.keys().forEach(filename => req(filename)); 24 | } 25 | 26 | configure(loadStories, module); 27 | -------------------------------------------------------------------------------- /.storybook/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: none !important; 3 | } 4 | 5 | code { 6 | margin: 0 0.2em; 7 | padding: 0.2em 0.4em 0.1em; 8 | font-size: 75%; 9 | background: rgba(218, 218, 218, 0.38); 10 | border: 1px solid #c5c5c5; 11 | -webkit-border-radius: 3px; 12 | -moz-border-radius: 3px; 13 | border-radius: 3px; 14 | font-weight: bold; 15 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, 16 | monospace; 17 | } 18 | 19 | li { 20 | margin-bottom: 8px; 21 | } 22 | 23 | button:not[class*="MuiButton"] { 24 | line-height: 1.499; 25 | position: relative; 26 | display: inline-block; 27 | font-weight: 400; 28 | white-space: nowrap; 29 | text-align: center; 30 | background-image: none; 31 | border: 1px solid transparent; 32 | -webkit-box-shadow: 0 2px 0 rgba(0, 0, 0, 0.015); 33 | box-shadow: 0 2px 0 rgba(0, 0, 0, 0.015); 34 | cursor: pointer; 35 | -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); 36 | transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); 37 | -webkit-user-select: none; 38 | -moz-user-select: none; 39 | -ms-user-select: none; 40 | user-select: none; 41 | -ms-touch-action: manipulation; 42 | touch-action: manipulation; 43 | height: 32px; 44 | padding: 0 15px; 45 | font-size: 14px; 46 | border-radius: 4px; 47 | color: rgba(0, 0, 0, 0.65); 48 | background-color: #fff; 49 | border-color: #d9d9d9; 50 | } 51 | 52 | button:not[class*="MuiButton"]:hover { 53 | outline: 0; 54 | color: #f50057; 55 | background-color: #fff; 56 | border-color: #f50057; 57 | } 58 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 5 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This project is moved to [mui-treasury](https://github.com/siriwatknp/mui-treasury/tree/master/packages/mui-layout) 2 | 3 | logo 4 | 5 | # Material-UI Layout [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-green.svg)](https://github.com/siriwatknp/mui-layout/pulls) 6 | 7 | A set of components that allows you to build dynamic and responsive layout based on Material-UI 8 | 9 | ## Prerequisites 10 | This project based on [React Material-UI](https://material-ui.com/), so you have to install `@material-ui/core @material-ui/styles` 11 | 12 | ## Installation 13 | 14 | ```bash 15 | // yarn 16 | yarn add mui-layout @material-ui/core @material-ui/styles @material-ui/icons 17 | 18 | // npm 19 | npm install mui-layout @material-ui/core @material-ui/styles @material-ui/icons 20 | ``` 21 | 22 | ## Demo 23 | see demo here [Storybook Demo](https://siriwatknp.github.io/mui-layout/?path=/story/welcome--introduction) 24 | 25 | 26 | ## Usage 27 | 28 | ```jsx 29 | // this example use icon from material-ui/icons, you can use your own! 30 | import React from 'react'; 31 | import { ThemeProvider } from '@material-ui/styles'; 32 | import { createMuiTheme } from '@material-ui/core/styles'; 33 | import ChevronLeft from '@material-ui/icons/ChevronLeft'; 34 | import ChevronRight from '@material-ui/icons/ChevronRight'; 35 | import MenuRounded from '@material-ui/icons/MenuRounded'; 36 | 37 | import { 38 | Root, 39 | Header, 40 | Nav, 41 | Content, 42 | Footer, 43 | presets, 44 | } from 'mui-layout'; 45 | 46 | const baseTheme = createMuiTheme(); // or use your own theme; 47 | const config = presets.createStandardLayout(); 48 | 49 | const App = () => ( 50 | 51 | 52 |
(open ? : )} 54 | > 55 | header 56 |
57 | 64 | 65 | content 66 | 67 |
68 | footer 69 |
70 |
71 |
72 | ) 73 | 74 | export default App; 75 | ``` 76 | 77 | ## Built-in Features 78 | - Collapsible Nav 79 | 80 | ![Alt Text](https://media.giphy.com/media/1BgIQWDxSNQHZS0HiN/giphy.gif) 81 | - Header Magnet 82 | 83 | ![alt text](https://media.giphy.com/media/L0ZQCiCrFiVKaHb5St/giphy.gif) 84 | - Auto Collapsed 85 | 86 | ![alt text](https://media.giphy.com/media/XbySngD0dtVnHeDq1a/giphy.gif) 87 | - Responsive Config 88 | ``` 89 | const extendedConfigs2 = { 90 | ...defaultConfig, 91 | 92 | // navVariant is 'temporary' in mobile and tablet, 'permanent' in desktop and greater 93 | navVariant: { 94 | xs: 'temporary', 95 | md: 'permanent', 96 | }, 97 | ``` 98 | 99 | ## Custom Styles 100 | Mostly, you will custom `Header` & `Nav`. This is an example for `Header` 101 | 102 | [Explanation is in storybook](https://siriwatknp.github.io/mui-layout/?path=/story/custom-styles--header) 103 | 104 | ```js 105 | import { makeStyles } from '@material-ui/styles'; 106 | 107 | const useHeaderStyles = makeStyles(({ palette, spacing }) => ({ 108 | header: { 109 | backgroundColor: palette.secondary.main, 110 | }, 111 | menuBtn: { 112 | padding: spacing(2.5), 113 | borderRadius: 0, 114 | }, 115 | icon: { 116 | color: palette.common.white, 117 | }, 118 | toolbar: { 119 | padding: spacing(0, 1), 120 | }, 121 | })); 122 | 123 | function App() { 124 | const { 125 | icon: iconCss, 126 | toolbar: toolbarCss, 127 | header: headerCss, 128 | menuBtn: menuBtnCss, 129 | } = useHeaderStyles(); 130 | return ( 131 |
134 | open ? ( 135 | 136 | ) : ( 137 | 138 | ) 139 | } 140 | menuButtonProps={{ className: menuBtnCss }} 141 | toolbarProps={{ className: toolbarCss }} 142 | /> 143 | ); 144 | } 145 | ``` 146 | 147 | ## Presets 148 | - Standard 149 | 150 | ![Alt Text](https://media.giphy.com/media/1jXGr4qb8dVizIUudS/giphy.gif) 151 | - Fixed 152 | 153 | ![Alt Text](https://media.giphy.com/media/fnW25ZYsCtCyrX2aho/giphy.gif) 154 | - Content Based 155 | 156 | ![Alt Text](https://media.giphy.com/media/1ZnFrQUZpCibwtTGj9/giphy.gif) 157 | - Cozy 158 | 159 | ![Alt Text](https://media.giphy.com/media/w9d1LsOBFndXpzV62z/giphy.gif) 160 | 161 | ## How it works 162 | - They are basically material-ui components that are combined to make things easier. 163 | `AppBar`, `Toolbar`, `Drawer` 164 | 165 | - use `@material-ui/styles` to style components 166 | 167 | - use react-hooks 168 | 169 | ## Contributing 170 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 171 | 172 | Please make sure to update tests as appropriate. 173 | 174 | ## License 175 | [MIT](https://choosealicense.com/licenses/mit/) 176 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src" 4 | }, 5 | "include": ["src"] 6 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mui-layout", 3 | "version": "1.2.4", 4 | "description": "Instantly build dynamic and responsive layout based on React Material-UI", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/siriwatknp/mui-layout.git", 7 | "author": "siriwatknp ", 8 | "license": "MIT", 9 | "husky": { 10 | "hooks": { 11 | "pre-commit": "yarn lint-staged" 12 | } 13 | }, 14 | "lint-staged": { 15 | "*src/**/*.{js,jsx}": [ 16 | "eslint" 17 | ] 18 | }, 19 | "scripts": { 20 | "build": "rm -rf dist && babel src --out-dir dist --copy-files", 21 | "prepublish": "yarn build", 22 | "storybook": "start-storybook -p 6006", 23 | "build-storybook": "build-storybook", 24 | "deploy-storybook": "storybook-to-ghpages" 25 | }, 26 | "devDependencies": { 27 | "@babel/cli": "^7.4.4", 28 | "@babel/core": "^7.4.5", 29 | "@babel/preset-env": "^7.4.5", 30 | "@babel/preset-react": "^7.0.0", 31 | "@material-ui/core": "^4.2.0", 32 | "@material-ui/icons": "^4.2.1", 33 | "@material-ui/styles": "^4.2.0", 34 | "@storybook/addon-actions": "^5.1.9", 35 | "@storybook/addon-links": "^5.1.9", 36 | "@storybook/addon-notes": "^5.1.9", 37 | "@storybook/addons": "^5.1.9", 38 | "@storybook/react": "^5.1.9", 39 | "@storybook/storybook-deployer": "^2.8.1", 40 | "babel-eslint": "^10.0.2", 41 | "babel-loader": "^8.0.6", 42 | "babel-plugin-module-resolver": "^3.2.0", 43 | "color": "^3.1.2", 44 | "eslint": "^5.12.0", 45 | "eslint-config-airbnb": "^17.1.0", 46 | "eslint-config-prettier": "^3.5.0", 47 | "eslint-plugin-import": "^2.14.0", 48 | "eslint-plugin-jsx-a11y": "^6.1.2", 49 | "eslint-plugin-prettier": "^3.0.1", 50 | "eslint-plugin-react": "^7.12.4", 51 | "husky": "^1.3.1", 52 | "lint-staged": "^8.1.0", 53 | "lodash": "^4.17.11", 54 | "prettier": "^1.15.3", 55 | "prop-types": "^15.7.2", 56 | "react": "^16.8.6", 57 | "react-dom": "^16.8.6", 58 | "react-gist": "^1.2.1", 59 | "react-router-dom": "^5.0.1", 60 | "storybook-react-router": "^1.0.5" 61 | }, 62 | "dependencies": {} 63 | } 64 | -------------------------------------------------------------------------------- /src/components/Content.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { makeStyles } from '@material-ui/styles'; 4 | import Box from '@material-ui/core/Box'; 5 | import useConfig from 'hooks/useConfig'; 6 | 7 | const getMargin = ({ 8 | navAnchor, 9 | navVariant, 10 | navWidth, 11 | collapsible, 12 | collapsed, 13 | collapsedWidth, 14 | opened, 15 | }) => { 16 | if (navAnchor !== 'left') return 0; 17 | if (navVariant === 'persistent' && opened) { 18 | // open is effect only when 19 | // navVariant === 'persistent' || 20 | // navVariant === 'temporary' 21 | return navWidth; 22 | } 23 | if (navVariant === 'permanent') { 24 | if (collapsible) { 25 | if (collapsed) return collapsedWidth; 26 | return navWidth; 27 | } 28 | return navWidth; 29 | } 30 | return 0; 31 | }; 32 | const getWidth = ({ opened, navVariant, squeezed }) => { 33 | if (navVariant === 'persistent' && opened) { 34 | // open is effect only when 35 | // navVariant === 'persistent' || 36 | // navVariant === 'temporary' 37 | if (squeezed) { 38 | return 'auto'; 39 | } 40 | return '100%'; 41 | } 42 | return 'auto'; 43 | }; 44 | const getHeight = ({ headerPosition, initialAdjustmentHeight }) => { 45 | if (headerPosition === 'fixed' || headerPosition === 'absolute') 46 | return initialAdjustmentHeight; 47 | return 0; 48 | }; 49 | 50 | const useStyles = makeStyles(({ transitions }) => ({ 51 | root: { 52 | flexGrow: 1, 53 | transition: transitions.create(['margin'], { 54 | easing: transitions.easing.sharp, 55 | duration: transitions.duration.leavingScreen, 56 | }), 57 | }, 58 | })); 59 | 60 | const Content = ({ 61 | component: Component, 62 | className, 63 | children, 64 | style, 65 | ...props 66 | }) => { 67 | const ctx = useConfig(); 68 | const classes = useStyles(props); 69 | return ( 70 | <> 71 | 72 | 81 | {typeof children === 'function' ? children(ctx) : children} 82 | 83 | 84 | ); 85 | }; 86 | 87 | Content.propTypes = { 88 | className: PropTypes.string, 89 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, 90 | component: PropTypes.elementType, 91 | style: PropTypes.shape({}), 92 | }; 93 | Content.defaultProps = { 94 | className: '', 95 | component: 'main', 96 | style: {}, 97 | }; 98 | 99 | export default Content; 100 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { makeStyles } from '@material-ui/styles'; 4 | import useConfig from 'hooks/useConfig'; 5 | 6 | const useStyles = makeStyles( 7 | ({ breakpoints, palette, spacing, transitions }) => ({ 8 | root: { 9 | borderTop: '1px solid', 10 | borderColor: palette.grey[200], 11 | padding: spacing(2), 12 | [breakpoints.up('sm')]: { 13 | padding: spacing(3), 14 | }, 15 | transition: transitions.create(['margin'], { 16 | easing: transitions.easing.sharp, 17 | duration: transitions.duration.leavingScreen, 18 | }), 19 | }, 20 | }), 21 | ); 22 | 23 | const Footer = ({ className, component: Component, style, ...props }) => { 24 | const ctx = useConfig(); 25 | const { 26 | navVariant, 27 | navWidth, 28 | collapsible, 29 | collapsed, 30 | collapsedWidth, 31 | footerShrink, 32 | open, 33 | navAnchor, 34 | } = ctx; 35 | const getMargin = () => { 36 | if (navAnchor !== 'left' || !footerShrink) return 0; 37 | if (navVariant === 'persistent' && open) { 38 | // open is effect only when 39 | // navVariant === 'persistent' || 40 | // navVariant === 'temporary' 41 | return navWidth; 42 | } 43 | if (navVariant === 'permanent') { 44 | if (collapsible) { 45 | if (collapsed) return collapsedWidth; 46 | return navWidth; 47 | } 48 | return navWidth; 49 | } 50 | return 0; 51 | }; 52 | const classes = useStyles(props); 53 | return ( 54 | 62 | ); 63 | }; 64 | 65 | Footer.propTypes = { 66 | className: PropTypes.string, 67 | component: PropTypes.elementType, 68 | style: PropTypes.shape({}), 69 | }; 70 | Footer.defaultProps = { 71 | className: '', 72 | component: 'footer', 73 | style: {}, 74 | }; 75 | 76 | export default Footer; 77 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { makeStyles } from '@material-ui/styles'; 4 | import AppBar from '@material-ui/core/AppBar'; 5 | import Toolbar from '@material-ui/core/Toolbar'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import useConfig from 'hooks/useConfig'; 8 | 9 | const createGet = ( 10 | { clipped, navVariant, collapsible, collapsed, opened, squeezed, navAnchor }, 11 | normal, 12 | shrink, 13 | pushed, 14 | unsqueeze, 15 | ) => () => { 16 | if (clipped || navAnchor !== 'left') return normal; 17 | if (navVariant === 'persistent' && opened) { 18 | // opened is effect only when 19 | // navVariant === 'persistent' || 20 | // navVariant === 'temporary' 21 | if (squeezed) { 22 | return pushed; 23 | } 24 | return unsqueeze; 25 | } 26 | if (navVariant === 'permanent') { 27 | if (collapsible) { 28 | if (collapsed) return shrink; 29 | return pushed; 30 | } 31 | return pushed; 32 | } 33 | return normal; 34 | }; 35 | 36 | const useStyles = makeStyles(({ zIndex, transitions }) => ({ 37 | root: ({ clipped }) => ({ 38 | zIndex: clipped ? zIndex.drawer + 1 : zIndex.appBar, 39 | transition: transitions.create(['margin', 'width'], { 40 | easing: transitions.easing.sharp, 41 | duration: transitions.duration.leavingScreen, 42 | }), 43 | }), 44 | })); 45 | 46 | const useMenuButtonStyles = makeStyles(({ spacing }) => ({ 47 | root: { 48 | marginLeft: spacing(-1), 49 | marginRight: spacing(1), 50 | }, 51 | })); 52 | 53 | const Header = ({ 54 | style, 55 | children, 56 | toolbarProps, 57 | renderMenuIcon, 58 | menuButtonProps, 59 | ...props 60 | }) => { 61 | const ctx = useConfig(); 62 | const { 63 | clipped, 64 | collapsedWidth, 65 | navWidth, 66 | navVariant, 67 | headerPosition, 68 | opened, 69 | setOpened, 70 | } = ctx; 71 | const getWidth = createGet( 72 | ctx, 73 | '100%', 74 | `calc(100% - ${collapsedWidth}px)`, 75 | `calc(100% - ${navWidth}px)`, 76 | '100%', 77 | ); 78 | const getMargin = createGet(ctx, 0, collapsedWidth, navWidth, navWidth); 79 | const shouldRenderMenu = navVariant !== 'permanent' && !!renderMenuIcon; 80 | const classes = useStyles({ ...props, clipped }); 81 | const menuButtonClasses = useMenuButtonStyles(menuButtonProps); 82 | return ( 83 | 95 | 96 | {shouldRenderMenu && ( 97 | setOpened(!opened)} 99 | {...menuButtonProps} 100 | classes={menuButtonClasses} 101 | > 102 | {renderMenuIcon(opened, setOpened)} 103 | 104 | )} 105 | {typeof children === 'function' ? children(ctx) : children} 106 | 107 | 108 | ); 109 | }; 110 | 111 | Header.propTypes = { 112 | style: PropTypes.shape({}), 113 | children: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), 114 | toolbarProps: PropTypes.shape({}), 115 | renderMenuIcon: PropTypes.func, 116 | menuButtonProps: PropTypes.shape({}), 117 | }; 118 | Header.defaultProps = { 119 | style: {}, 120 | children: null, 121 | toolbarProps: {}, 122 | renderMenuIcon: null, 123 | menuButtonProps: {}, 124 | }; 125 | 126 | export default Header; 127 | -------------------------------------------------------------------------------- /src/components/Nav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { makeStyles } from '@material-ui/styles'; 4 | import Box from '@material-ui/core/Box'; 5 | import Drawer from '@material-ui/core/Drawer'; 6 | import Button from '@material-ui/core/Button'; 7 | import IconButton from '@material-ui/core/IconButton'; 8 | import Grow from '@material-ui/core/Grow'; 9 | import useConfig from 'hooks/useConfig'; 10 | import useHeightAdjustment from 'hooks/useHeightAdjustment'; 11 | import useAutoCollapse from 'hooks/useAutoCollapse'; 12 | 13 | const useStyles = makeStyles( 14 | ({ breakpoints, transitions, palette, spacing, zIndex, shadows }) => ({ 15 | root: {}, 16 | heightAdjustment: { 17 | flexShrink: 0, 18 | transition: transitions.create(), 19 | }, 20 | container: { 21 | overflow: 'hidden', 22 | display: 'flex', 23 | flexGrow: 1, 24 | flexDirection: 'column', 25 | transition: transitions.create(['width'], { 26 | easing: transitions.easing.sharp, 27 | duration: transitions.duration.leavingScreen, 28 | }), 29 | }, 30 | content: { 31 | flexGrow: 1, 32 | overflowX: 'hidden', 33 | overflowY: 'auto', 34 | }, 35 | toggleButton: { 36 | backgroundColor: palette.grey[50], 37 | textAlign: 'center', 38 | borderRadius: 0, 39 | borderTop: '1px solid', 40 | borderColor: 'rgba(0,0,0,0.12)', 41 | [breakpoints.up('sm')]: { 42 | minHeight: 40, 43 | }, 44 | }, 45 | closeButton: { 46 | position: 'absolute', 47 | bottom: spacing(2), 48 | zIndex: zIndex.modal + 1, 49 | background: palette.common.white, 50 | boxShadow: shadows[2], 51 | '@media (hover: none)': { 52 | backgroundColor: palette.grey[300], 53 | }, 54 | '&:hover': { 55 | backgroundColor: '#e5e5e5', 56 | }, 57 | }, 58 | }), 59 | ); 60 | 61 | const Nav = ({ 62 | className, 63 | header, 64 | children, 65 | renderIcon, 66 | toggleProps, 67 | ...props 68 | }) => { 69 | useAutoCollapse(); 70 | const ctx = useConfig(); 71 | const { 72 | opened, 73 | setOpened, 74 | navVariant, 75 | navAnchor, 76 | navWidth, 77 | collapsedWidth, 78 | collapsible, 79 | collapsed, 80 | setCollapsed, 81 | } = ctx; 82 | const height = useHeightAdjustment(); 83 | const getWidth = () => { 84 | if (collapsible && collapsed) return collapsedWidth; 85 | return navWidth; 86 | }; 87 | const shouldRenderButton = collapsible && renderIcon; 88 | const classes = useStyles(props); 89 | return ( 90 | 91 | setOpened(false)} 96 | variant={navVariant} 97 | anchor={navAnchor} 98 | > 99 |
100 | 101 | {typeof header === 'function' ? header(ctx) : header} 102 |
103 | {typeof children === 'function' ? children(ctx) : children} 104 |
105 | {shouldRenderButton && ( 106 | 114 | )} 115 |
116 |
117 | 118 | setOpened(false)} 122 | > 123 | {renderIcon(false)} 124 | 125 | 126 |
127 | ); 128 | }; 129 | 130 | Nav.propTypes = { 131 | className: PropTypes.string, 132 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, 133 | header: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), 134 | renderIcon: PropTypes.func, 135 | toggleProps: PropTypes.shape({}), 136 | }; 137 | Nav.defaultProps = { 138 | className: '', 139 | header: null, 140 | renderIcon: null, 141 | toggleProps: {}, 142 | }; 143 | 144 | export default Nav; 145 | -------------------------------------------------------------------------------- /src/components/Root.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import CssBaseline from '@material-ui/core/CssBaseline'; 4 | import LayoutContext from 'context'; 5 | import { createDefaultLayout } from 'utils/presets'; 6 | import useMergedConfig from 'hooks/useMergedConfig'; 7 | import useWidth from 'hooks/useWidth'; 8 | 9 | const initialConfig = createDefaultLayout(); 10 | 11 | // eslint-disable-next-line react/prop-types 12 | const Root = ({ children, config }) => { 13 | const [collapsed, setCollapsed] = useState(false); 14 | const [opened, setOpened] = useState(false); 15 | const mergedConfig = useMergedConfig(config, initialConfig); 16 | const screen = useWidth(); // screen could be null at first render 17 | return ( 18 | 28 | 29 | {children} 30 | 31 | ); 32 | }; 33 | 34 | const createScreenPropTypes = valPropTypes => 35 | PropTypes.shape({ 36 | xs: valPropTypes, 37 | sm: valPropTypes, 38 | md: valPropTypes, 39 | lg: valPropTypes, 40 | xl: valPropTypes, 41 | }); 42 | Root.propTypes = { 43 | // from HOC 44 | // general 45 | config: PropTypes.shape({ 46 | clipped: PropTypes.oneOfType([ 47 | PropTypes.bool, 48 | createScreenPropTypes(PropTypes.bool), 49 | ]), 50 | collapsible: PropTypes.oneOfType([ 51 | PropTypes.bool, 52 | createScreenPropTypes(PropTypes.bool), 53 | ]), 54 | collapsedWidth: PropTypes.oneOfType([ 55 | PropTypes.number, 56 | createScreenPropTypes(PropTypes.number), 57 | ]), 58 | collapsed: PropTypes.bool, 59 | navVariant: PropTypes.oneOfType([ 60 | PropTypes.oneOf(['permanent', 'persistent', 'temporary']), 61 | createScreenPropTypes( 62 | PropTypes.oneOf(['permanent', 'persistent', 'temporary']), 63 | ), 64 | ]), 65 | navWidth: PropTypes.oneOfType([ 66 | PropTypes.number, 67 | createScreenPropTypes(PropTypes.number), 68 | ]), 69 | navAnchor: PropTypes.oneOfType([ 70 | PropTypes.oneOf(['left', 'bottom']), 71 | createScreenPropTypes(PropTypes.oneOf(['left', 'bottom'])), 72 | ]), 73 | headerPosition: PropTypes.oneOfType([ 74 | PropTypes.oneOf(['static', 'relative', 'sticky', 'fixed', 'absolute']), 75 | createScreenPropTypes( 76 | PropTypes.oneOf(['static', 'relative', 'sticky', 'fixed', 'absolute']), 77 | ), 78 | ]), 79 | squeezed: PropTypes.oneOfType([ 80 | PropTypes.bool, 81 | createScreenPropTypes(PropTypes.bool), 82 | ]), 83 | footerShrink: PropTypes.oneOfType([ 84 | PropTypes.bool, 85 | createScreenPropTypes(PropTypes.bool), 86 | ]), 87 | }), 88 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, 89 | }; 90 | Root.defaultProps = { 91 | config: initialConfig, 92 | }; 93 | 94 | export default Root; 95 | -------------------------------------------------------------------------------- /src/context/LayoutContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDefaultLayout } from 'utils/presets'; 3 | 4 | const LayoutContext = React.createContext({ 5 | opened: false, 6 | setOpened: () => {}, 7 | collapsed: false, 8 | setCollapsed: () => {}, 9 | ...createDefaultLayout(), 10 | }); 11 | 12 | export default LayoutContext; 13 | -------------------------------------------------------------------------------- /src/context/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './LayoutContext'; 2 | -------------------------------------------------------------------------------- /src/hooks/useAutoCollapse.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useTheme } from '@material-ui/core'; 3 | import useConfig from './useConfig'; 4 | 5 | export default () => { 6 | const { 7 | breakpoints: { keys }, 8 | } = useTheme(); 9 | const { 10 | collapsible, 11 | screen, 12 | collapsed, 13 | setCollapsed, 14 | collapsedBreakpoint, 15 | autoCollapsedDisabled, 16 | } = useConfig(); 17 | 18 | useEffect(() => { 19 | // skip everything if user disable this feature in config 20 | if (!autoCollapsedDisabled) { 21 | if (collapsible && screen) { 22 | if (collapsed && screen === collapsedBreakpoint) { 23 | setCollapsed(false); 24 | } 25 | if ( 26 | !collapsed && 27 | keys.indexOf(screen) < keys.indexOf(collapsedBreakpoint) 28 | ) { 29 | setCollapsed(true); 30 | } 31 | } 32 | } 33 | }, [screen]); 34 | if (autoCollapsedDisabled) { 35 | return null; 36 | } 37 | return true; 38 | }; 39 | -------------------------------------------------------------------------------- /src/hooks/useConfig.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import LayoutContext from 'context'; 3 | 4 | export default () => useContext(LayoutContext); 5 | -------------------------------------------------------------------------------- /src/hooks/useEventListener.js: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react'; 2 | 3 | function useEventListener(eventName, handler, element) { 4 | if (!element && typeof window !== 'undefined') { 5 | element = window; 6 | } 7 | 8 | // Create a ref that stores handler 9 | const savedHandler = useRef(); 10 | 11 | // Update ref.current value if handler changes. 12 | // This allows our effect below to always get latest handler ... 13 | // ... without us needing to pass it in effect deps array ... 14 | // ... and potentially cause effect to re-run every render. 15 | useEffect(() => { 16 | savedHandler.current = handler; 17 | }, [handler]); 18 | 19 | useEffect( 20 | () => { 21 | // Make sure element supports addEventListener 22 | // On 23 | const isSupported = element && element.addEventListener; 24 | if (!isSupported) return; 25 | 26 | // Create event listener that calls handler function stored in ref 27 | const eventListener = event => savedHandler.current(event); 28 | 29 | // Add event listener 30 | element.addEventListener(eventName, eventListener); 31 | 32 | // Remove event listener on cleanup 33 | // eslint-disable-next-line consistent-return 34 | return () => { 35 | element.removeEventListener(eventName, eventListener); 36 | }; 37 | }, 38 | [eventName, element], // Re-run if eventName or element changes 39 | ); 40 | } 41 | 42 | export default useEventListener; 43 | -------------------------------------------------------------------------------- /src/hooks/useHeightAdjustment.js: -------------------------------------------------------------------------------- 1 | import debounce from 'lodash/debounce'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | import { useTheme } from '@material-ui/core'; 4 | import createGetScreenValue from 'utils/createGetScreenValue'; 5 | import someIs from 'utils/someIs'; 6 | import useConfig from './useConfig'; 7 | import useWidth from './useWidth'; 8 | import useEventListener from './useEventListener'; 9 | 10 | export default () => { 11 | const { 12 | breakpoints: { keys }, 13 | } = useTheme(); 14 | const { 15 | clipped, 16 | headerPosition, 17 | initialAdjustmentHeight, 18 | heightAdjustmentSpeed, 19 | heightAdjustmentDisabled, 20 | navVariant, 21 | } = useConfig(); 22 | const currentScreen = useWidth(); 23 | const getScreenValue = createGetScreenValue(keys, currentScreen); 24 | const initialHeight = getScreenValue(initialAdjustmentHeight); 25 | 26 | const [height, setHeight] = useState(0); 27 | 28 | useEffect(() => { 29 | if (typeof initialHeight === 'number') { 30 | setHeight(initialHeight); 31 | } 32 | }, [initialHeight]); 33 | 34 | const handler = useCallback( 35 | debounce( 36 | () => { 37 | // Update height 38 | if (typeof initialHeight === 'number') { 39 | const offset = initialHeight - window.scrollY; 40 | setHeight(offset < 0 ? 0 : offset); 41 | } 42 | }, 43 | heightAdjustmentSpeed, 44 | { leading: true, trailing: true }, 45 | ), 46 | [setHeight, initialHeight], 47 | ); 48 | 49 | useEventListener('scroll', handler); 50 | 51 | if (heightAdjustmentDisabled) return 0; // disabled by user. 52 | if (navVariant === 'temporary') return 0; 53 | if (!clipped) { 54 | // do not run the effect below if behavior is not right. 55 | return 0; 56 | } 57 | if (clipped && someIs(['sticky', 'fixed'], headerPosition)) { 58 | return initialHeight; 59 | } 60 | return height; 61 | }; 62 | -------------------------------------------------------------------------------- /src/hooks/useMergedConfig.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useTheme } from '@material-ui/core/styles'; 3 | import createGetScreenValue from 'utils/createGetScreenValue'; 4 | import useConfig from './useConfig'; 5 | import useWidth from './useWidth'; 6 | 7 | export default (config, defaultConfig = {}) => { 8 | // if no config passed from params, use context instead. 9 | const context = config || useConfig(); 10 | const { 11 | breakpoints: { keys }, 12 | } = useTheme(); 13 | const currentScreen = useWidth(); 14 | const contextKeys = Object.keys(context); 15 | const getScreenValue = createGetScreenValue(keys, currentScreen); 16 | const assignValue = () => { 17 | const screenContext = {}; 18 | // eslint-disable-next-line no-plusplus 19 | for (let i = 0; i < contextKeys.length; i++) { 20 | const key = contextKeys[i]; 21 | screenContext[key] = getScreenValue(context[key], defaultConfig[key]); 22 | } 23 | return screenContext; 24 | }; 25 | return useMemo(assignValue, [context, currentScreen]); 26 | }; 27 | -------------------------------------------------------------------------------- /src/hooks/useWidth.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import { useTheme } from '@material-ui/styles'; 3 | import useMediaQuery from '@material-ui/core/useMediaQuery'; 4 | /** 5 | * Be careful using this hook. It only works because the number of 6 | * breakpoints in theme is static. It will break once you change the number of 7 | * breakpoints. See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level 8 | */ 9 | 10 | function useWidth() { 11 | const theme = useTheme(); 12 | const isXs = useMediaQuery(theme.breakpoints.up('xs')); 13 | const isSm = useMediaQuery(theme.breakpoints.up('sm')); 14 | const isMd = useMediaQuery(theme.breakpoints.up('md')); 15 | const isLg = useMediaQuery(theme.breakpoints.up('lg')); 16 | const isXl = useMediaQuery(theme.breakpoints.up('xl')); 17 | if (isXl) return 'xl'; 18 | if (isLg) return 'lg'; 19 | if (isMd) return 'md'; 20 | if (isSm) return 'sm'; 21 | if (isXs) return 'xs'; 22 | return null; 23 | } 24 | 25 | export default useWidth; 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as LayoutContext } from './context'; 2 | export { default as Root } from './components/Root'; 3 | export { default as Header } from './components/Header'; 4 | export { default as Nav } from './components/Nav'; 5 | export { default as Content } from './components/Content'; 6 | export { default as Footer } from './components/Footer'; 7 | export { default as presets } from './utils/presets'; 8 | export { default as useConfig } from './hooks/useConfig'; 9 | export { default as useWidth } from './hooks/useWidth'; 10 | export { default as useHeightAdjustment } from './hooks/useHeightAdjustment'; 11 | export { default as useAutoCollapse } from './hooks/useAutoCollapse'; 12 | export * from './utils/presets'; 13 | -------------------------------------------------------------------------------- /src/utils/createGetScreenValue.js: -------------------------------------------------------------------------------- 1 | import isPlainObject from './isPlainObject'; 2 | 3 | const createGetScreenValue = (keys, currentScreen) => (value, defaultValue) => { 4 | if (value !== null && value !== undefined) { 5 | if (!isPlainObject(value)) { 6 | return value; 7 | } 8 | let index = keys.indexOf(currentScreen); 9 | while (index >= 0) { 10 | if (value[keys[index]] !== undefined) { 11 | return value[keys[index]]; 12 | } 13 | index -= 1; 14 | } 15 | } 16 | if (!defaultValue) return value; 17 | return createGetScreenValue(keys, currentScreen)(defaultValue); 18 | }; 19 | 20 | export default createGetScreenValue; 21 | -------------------------------------------------------------------------------- /src/utils/get.js: -------------------------------------------------------------------------------- 1 | const get = (object, path, defaultVal) => { 2 | const PATH = Array.isArray(path) 3 | ? path 4 | : path.split('.').filter(i => i.length); 5 | if (!PATH.length) { 6 | return object === undefined ? defaultVal : object; 7 | } 8 | if ( 9 | object === null || 10 | object === undefined || 11 | typeof object[PATH[0]] === 'undefined' 12 | ) { 13 | return defaultVal; 14 | } 15 | return this.get(object[PATH.shift()], PATH, defaultVal); 16 | }; 17 | 18 | export default get; 19 | -------------------------------------------------------------------------------- /src/utils/getWindowSizes.js: -------------------------------------------------------------------------------- 1 | const getWindowSizes = () => { 2 | const canUseDOM = typeof window !== 'undefined'; 3 | 4 | return { 5 | width: canUseDOM ? window.innerWidth : null, 6 | height: canUseDOM ? window.innerHeight : null, 7 | canUseDOM, 8 | }; 9 | }; 10 | 11 | export default getWindowSizes; 12 | -------------------------------------------------------------------------------- /src/utils/isPlainObject.js: -------------------------------------------------------------------------------- 1 | const isPlainObject = obj => 2 | Object.prototype.toString.call(obj) === '[object Object]'; 3 | 4 | export default isPlainObject; 5 | -------------------------------------------------------------------------------- /src/utils/presets.js: -------------------------------------------------------------------------------- 1 | export const createDefaultLayout = config => ({ 2 | navWidth: 256, 3 | navAnchor: 'left', 4 | navVariant: 'temporary', 5 | collapsible: false, 6 | collapsedWidth: 64, 7 | collapsedBreakpoint: 'md', 8 | autoCollapsedDisabled: false, 9 | clipped: false, 10 | heightAdjustmentDisabled: false, 11 | initialAdjustmentHeight: { xs: 56, sm: 64 }, // toolbar's height in xs is 56px 12 | heightAdjustmentSpeed: 144, 13 | headerPosition: 'relative', 14 | squeezed: false, 15 | footerShrink: true, 16 | ...config, 17 | }); 18 | 19 | export const createStandardLayout = config => ({ 20 | ...createDefaultLayout(), 21 | clipped: true, 22 | navVariant: { 23 | xs: 'temporary', 24 | sm: 'permanent', 25 | }, 26 | collapsible: { 27 | xs: false, 28 | sm: true, 29 | }, 30 | ...config, 31 | }); 32 | 33 | export const createFixedLayout = config => ({ 34 | ...createDefaultLayout(), 35 | navVariant: { 36 | xs: 'temporary', 37 | md: 'permanent', 38 | }, 39 | collapsible: { 40 | xs: false, 41 | md: true, 42 | }, 43 | squeezed: true, 44 | headerPosition: 'sticky', 45 | ...config, 46 | }); 47 | 48 | export const createContentBasedLayout = config => ({ 49 | ...createDefaultLayout(), 50 | navWidth: { 51 | sm: 200, 52 | md: 256, 53 | }, 54 | navVariant: { 55 | xs: 'temporary', 56 | sm: 'persistent', 57 | }, 58 | collapsible: false, 59 | ...config, 60 | }); 61 | 62 | export const createCozyLayout = config => ({ 63 | ...createDefaultLayout(), 64 | navVariant: { 65 | xs: 'persistent', 66 | sm: 'permanent', 67 | }, 68 | navWidth: { 69 | sm: 200, 70 | md: 256, 71 | xs: 64, 72 | }, 73 | collapsible: { 74 | xs: false, 75 | sm: true, 76 | }, 77 | clipped: false, 78 | ...config, 79 | }); 80 | 81 | export const createMuiTreasuryLayout = config => ({ 82 | ...createDefaultLayout(), 83 | navWidth: 200, 84 | navVariant: { 85 | xs: 'temporary', 86 | md: 'permanent', 87 | }, 88 | heightAdjustmentDisabled: true, 89 | clipped: true, 90 | collapsible: false, 91 | ...config, 92 | }); 93 | 94 | export default { 95 | createDefaultLayout, 96 | createStandardLayout, 97 | createFixedLayout, 98 | createContentBasedLayout, 99 | createCozyLayout, 100 | createMuiTreasuryLayout, 101 | }; 102 | -------------------------------------------------------------------------------- /src/utils/someIs.js: -------------------------------------------------------------------------------- 1 | export default (items, value) => items.some(item => item === value); 2 | -------------------------------------------------------------------------------- /stories/0welcome.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { linkTo } from '@storybook/addon-links'; 3 | import { storiesOf } from '@storybook/react'; 4 | import Gist from 'react-gist'; 5 | import Box from '@material-ui/core/Box'; 6 | import Divider from '@material-ui/core/Divider'; 7 | import Typography from '@material-ui/core/Typography'; 8 | import Link from '@material-ui/core/Link'; 9 | 10 | storiesOf('Welcome', module) 11 | .add('Introduction', () => ( 12 | 13 | INTRODUCING 14 | 15 | Material UI Layout 16 | 17 | 18 | Version 1.2 19 | 20 | 21 | Layout is a group of Material-UI components that are enhanced and 22 | combined to create dynamic, easy-to-maintain and easy-to-use as much as 23 | possible. 24 | 25 |
26 | 27 | {"Let's start with Why?"} 28 | 29 | 30 | I created this because 31 |
32 |
33 | 34 |
    35 |
  1. 36 | It took me a lot of time to initialize dashboard layout when I have 37 | new projects and I’m sure that a lot of you guys agree with me. 38 |
  2. 39 |
  3. 40 | Sometimes it is hard to refactor because the structure is so poor 41 | because someone isn’t deeply understand what he/she was doing, as a 42 | result, rewrite the whole layout (it actually happened, at least in 43 | my experience). 44 |
  4. 45 |
  5. 46 | Because we need to be fast to let others continue our work, we 47 | frequently write poor and a lot of code. However, we say we don’t 48 | have time to fix them. Eventually, spend all day paying technical 49 | debts. 50 |
  6. 51 |
52 |
53 |
54 | 55 | Objectives 56 | 57 | 58 | It must be easy enough to use, however still be able to adjust to compat 59 | with real word examples and usages. More importantly, it need to follow{' '} 60 | 66 | Material specs 67 | {' '} 68 | since we are 100% based on Material-UI with no other styling libraries. 69 | Last but not least, responsive is a must. 70 | 71 |
72 | 73 | Solution 74 | 75 | Separate layout into 5 components 76 | 77 |
    78 |
  • Root
  • 79 |
  • Header (AppBar)
  • 80 |
  • Nav (Drawer)
  • 81 |
  • Content
  • 82 |
  • Footer
  • 83 |
84 |
85 | 86 | Root will provide context to other components to sync states 87 | across them. 88 | 89 | 90 | Behavior of your layout will be controlled by using config (just 91 | a plain object) that will be injected to the Root. 92 | 93 | 94 | Presets is a set of predefined config that I can come up with 95 | after researching a lot of the real world websites and also the{' '} 96 | 101 | official Material specs 102 | 103 | 104 |
105 | 106 |
107 |
108 |
109 | )) 110 | .add('Features', () => ( 111 | 112 | 113 | Built-in Features 114 | 115 | 116 | with these built-in features, you don't have to do any tedious works to 117 | make it work! hooks is used internally. Moreover, if you don't like 118 | these build-in features, you can turn it off by pass some config to{' '} 119 | Root component 120 | 121 |
122 | 123 | 1. Collapsible Nav 124 | 125 | 126 | 127 |
    128 |
  • 129 | collapsible is boolean, false means you don't want this 130 | feature., it will hide menu icon at the bottom of the nav 131 |
  • 132 |
  • 133 | collapsedWidth is number or object (mediaQuery pattern) 134 | means the width after the nav collapsed. 135 |
  • 136 |
137 |
138 |
139 | 140 | 2. Header Magnet 141 | 142 | 143 | 144 |
    145 |
  • 146 | heightAdjustmentDisabled is boolean, true means you 147 | don't want this feature :( 148 |
  • 149 |
  • 150 | initialAdjustmentHeight is number or object (mediaQuery 151 | pattern) 152 |
  • 153 |
  • 154 | heightAdjustmentSpeed the speed of snapping (ms) the 155 | lower, the faster but drop performance 156 |
  • 157 |
158 |
159 |
160 | 161 | 3. Auto Collapsed 162 | 163 | 164 | 165 | Nav will collapsed automatically when it detects that the screen is 166 | below breakpoint. Likewise, it will stretch automatically when the 167 | screen is larger than breakpoint. Default value is "md". 168 | 169 | 170 |
    171 |
  • 172 | autoCollapsedDisabled is boolean, true means you don't 173 | want this feature :( 174 |
  • 175 |
  • 176 | collapsedBreakpoint is string one of ['sm', 'md', 177 | 'lg']. (sm = 600px, md = 960px best!, lg=1280px) 178 |
  • 179 |
180 |
181 |
182 | 183 |
184 | 185 | Excited? 186 | 187 | 188 |
189 | )) 190 | .add('Get started', () => ( 191 | 192 | 193 | 194 | 1. Easiest Way 195 | 196 | 197 | 198 | 206 | Open this sandbox 207 | 208 | {' '} 209 | to see the code. and then{' '} 210 | 213 | 214 |
215 | 216 |
217 | 218 | 2. Manual Way 219 | 220 | 221 | create-react-app is the fastest way to test and start building 222 | react app. 223 | 224 |
225 | 226 | 227 | 228 | then copy the code below and paste to App.js 229 | 230 | 231 | ThemeProvider is a must since mui-layout need to access{' '} 232 | breakpoints in theme. 233 | 234 | 235 | 236 | 237 | 238 | Congratulations! 239 | 240 | 241 | Next Step, Let's customize your layout's behavior by adjusting 242 | config 243 | 244 | 245 | 246 |
247 |
248 |
249 |
250 | )) 251 | .add('Configuration', () => ( 252 | 253 | 254 | 255 | Adjust Config 256 | 257 | 258 | config is just a plain object that defines behavior of your layout. 259 | Mostly, the value of each field can be an object pattern (mediaQuery). 260 | 261 | 262 | 263 | 264 | 265 | to see what each field can do{' '} 266 | 272 | Click here 273 | 274 | 275 | 276 |
277 |
278 | 279 | 280 | Object pattern 281 | 282 | 283 | 284 | 285 | 286 | if you want to customize behavior in each screen (mobile, tablet, 287 | desktop), you have to specify it in object. The key represent the 288 | screen that the value will be applied on, ex.{' '} 289 | {`\{ "sm": true \}`} means you want this behavior to be 290 | true in tablet. Luckily, you dont have to specify all screen key. if 291 | there is no key let say "md"(desktop), it will use the smaller 292 | specified value that it can found. If you want to apply to all screen, 293 | don't use object pattern. 294 | 295 | 296 |
297 |
298 | 299 | 300 | Example preset 301 | 302 | 303 | 304 |
305 |
306 | 307 | 308 | Pass as props 309 | 310 | 311 | pass your config as props to Root component 312 | 313 | 314 | 315 |
316 |
317 | 318 | 319 | Lazy? pick one from my presets (see demo on the panel) 320 | 321 | 322 | 323 |
324 |
325 |
326 | )); 327 | -------------------------------------------------------------------------------- /stories/components.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import ChevronLeft from '@material-ui/icons/ChevronLeft'; 5 | import ChevronRight from '@material-ui/icons/ChevronRight'; 6 | import MenuRounded from '@material-ui/icons/MenuRounded'; 7 | import Root from 'components/Root'; 8 | import Header from 'components/Header'; 9 | import Nav from 'components/Nav'; 10 | import Content from 'components/Content'; 11 | import Footer from 'components/Footer'; 12 | 13 | // MOCK 14 | import NavContentEx from './mock/NavContentEx'; 15 | import NavHeaderEx from './mock/NavHeaderEx'; 16 | import HeaderEx from './mock/HeaderEx'; 17 | import ContentEx from './mock/ContentEx'; 18 | import FooterEx from './mock/FooterEx'; 19 | 20 | // storiesOf('Welcome', module).add('to Storybook', () => ); 21 | 22 | storiesOf('Components', module) 23 | .add('Root', () => ( 24 | 25 | Root is just a Provider, it does not return any DOM on the 26 | screen. 27 | 28 | )) 29 | .add('Header', () => ( 30 | 31 |
(open ? : )} 33 | /> 34 | 35 | )) 36 | .add('Header (mock children)', () => ( 37 | 38 |
(open ? : )} 40 | > 41 | {({ screen, collapsed }) => ( 42 | 43 | )} 44 |
45 |
46 | )) 47 | .add('Nav', () => ( 48 | 49 |
(open ? : )} 51 | /> 52 |