Layout scaffolding for Web apps.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

466 lines
9.8 KiB

  1. import * as React from 'react';
  2. import styled, { createGlobalStyle } from 'styled-components'
  3. import TopBar from '../../widgets/TopBar'
  4. import {minWidthFactor} from '../../utilities/mixins';
  5. import {configVar, loadConfig} from '../../utilities/helpers'
  6. const Config = createGlobalStyle({
  7. ...loadConfig(),
  8. })
  9. const DisableScrolling = createGlobalStyle({
  10. 'body': {
  11. overflow: 'hidden',
  12. [minWidthFactor(3)]: {
  13. overflow: 'auto',
  14. },
  15. },
  16. })
  17. const ContentBase = styled('main')({
  18. boxSizing: 'border-box',
  19. paddingBottom: 'var(--size-menu, 4rem)',
  20. [minWidthFactor(3)]: {
  21. paddingLeft: `calc(50% - ${configVar('base-width')} * 0.5)`,
  22. paddingBottom: 0,
  23. },
  24. })
  25. const SidebarBase = styled('div')({
  26. boxSizing: 'border-box',
  27. overflow: 'hidden',
  28. display: 'contents',
  29. left: `calc(${configVar('base-width')} * -1)`,
  30. backgroundColor: 'var(--color-bg, white)',
  31. [minWidthFactor(3)]: {
  32. position: 'fixed',
  33. top: 0,
  34. left: 0,
  35. width: `calc(50% - ${configVar('base-width')} * 0.5)`,
  36. height: '100%',
  37. display: 'block',
  38. },
  39. })
  40. const SidebarMain = styled('div')({
  41. boxSizing: 'border-box',
  42. position: 'fixed',
  43. top: 0,
  44. right: '100%',
  45. width: '100%',
  46. height: '100%',
  47. overflow: 'auto',
  48. // overflow: 'overlay',
  49. paddingTop: 'inherit',
  50. paddingBottom: 'var(--size-menu, 4rem)',
  51. scrollbarWidth: 'none',
  52. '::-webkit-scrollbar': {
  53. display: 'none',
  54. },
  55. backgroundColor: 'var(--color-bg, white)',
  56. [minWidthFactor(3)]: {
  57. position: 'absolute',
  58. right: 0,
  59. width: `calc(${configVar('base-width')} - var(--size-menu, 4rem))`,
  60. marginLeft: 'auto',
  61. paddingBottom: 0,
  62. },
  63. })
  64. const OpenSidebarMain = styled(SidebarMain)({
  65. right: 0,
  66. })
  67. const SidebarMenu = styled('div')({
  68. boxSizing: 'border-box',
  69. overflow: 'auto',
  70. // overflow: 'overlay',
  71. scrollbarWidth: 'none',
  72. '::-webkit-scrollbar': {
  73. display: 'none',
  74. },
  75. position: 'fixed',
  76. bottom: 0,
  77. left: 0,
  78. width: '100%',
  79. height: 'var(--size-menu, 4rem)',
  80. zIndex: 1,
  81. backgroundColor: 'var(--color-bg, white)',
  82. [minWidthFactor(3)]: {
  83. top: 0,
  84. marginLeft: 'auto',
  85. position: 'absolute',
  86. height: '100%',
  87. paddingTop: 'inherit',
  88. overflow: 'auto',
  89. zIndex: 'auto',
  90. },
  91. })
  92. const SidebarMenuSize = styled('div')({
  93. display: 'flex',
  94. width: '100%',
  95. height: '100%',
  96. maxWidth: `calc(${configVar('base-width')} * 2)`,
  97. margin: '0 auto',
  98. [minWidthFactor(3)]: {
  99. maxWidth: 'none',
  100. marginRight: 0,
  101. flexDirection: 'column',
  102. justifyContent: 'space-between',
  103. alignItems: 'flex-end',
  104. },
  105. })
  106. const SidebarMenuGroup = styled('div')({
  107. display: 'contents',
  108. [minWidthFactor(3)]: {
  109. width: '100%',
  110. display: 'block',
  111. },
  112. })
  113. const MoreItems = styled('div')({
  114. position: 'fixed',
  115. top: 0,
  116. left: '-100%',
  117. width: '100%',
  118. height: '100%',
  119. paddingTop: 'var(--height-topbar, 4rem)',
  120. paddingBottom: 'var(--size-menu, 4rem)',
  121. zIndex: -1,
  122. boxSizing: 'border-box',
  123. backgroundColor: 'var(--color-bg, white)',
  124. [minWidthFactor(3)]: {
  125. display: 'contents',
  126. },
  127. })
  128. const OpenMoreItems = styled(MoreItems)({
  129. left: 0,
  130. })
  131. const MoreItemsScroll = styled('div')({
  132. width: '100%',
  133. height: '100%',
  134. overflow: 'auto',
  135. [minWidthFactor(3)]: {
  136. display: 'contents',
  137. },
  138. })
  139. const MorePrimarySidebarMenuGroup = styled(SidebarMenuGroup)({
  140. [minWidthFactor(3)]: {
  141. flex: 'auto',
  142. },
  143. })
  144. const MoreSecondarySidebarMenuGroup = styled(SidebarMenuGroup)({
  145. [minWidthFactor(3)]: {
  146. order: 4,
  147. },
  148. })
  149. const SidebarMenuItem = styled('span')({
  150. width: 0,
  151. flex: 'auto',
  152. height: 'var(--size-menu, 4rem)',
  153. '> *': {
  154. height: '100%',
  155. display: 'flex',
  156. alignItems: 'center',
  157. textDecoration: 'none',
  158. width: '100%',
  159. },
  160. [minWidthFactor(3)]: {
  161. width: 'auto !important',
  162. flex: '0 1 auto',
  163. '> *': {
  164. height: 'auto',
  165. },
  166. },
  167. })
  168. const MoreSidebarMenuItem = styled('span')({
  169. display: 'block',
  170. height: 'var(--size-menu, 4rem)',
  171. '> *': {
  172. height: '100%',
  173. display: 'flex',
  174. alignItems: 'center',
  175. textDecoration: 'none',
  176. width: '100%',
  177. },
  178. [minWidthFactor(3)]: {
  179. width: 'auto !important',
  180. flex: '0 1 auto',
  181. },
  182. })
  183. const MoreToggleSidebarMenuItem = styled(SidebarMenuItem)({
  184. [minWidthFactor(3)]: {
  185. display: 'none',
  186. },
  187. })
  188. export const SidebarMenuItemIcon = styled('span')({
  189. display: 'block',
  190. [minWidthFactor(3)]: {
  191. width: 'var(--size-menu, 4rem)',
  192. height: 'var(--size-menu, 4rem)',
  193. display: 'grid',
  194. placeContent: 'center',
  195. },
  196. })
  197. export const MoreSidebarMenuItemIcon = styled('span')({
  198. marginRight: '1rem',
  199. display: 'block',
  200. [minWidthFactor(3)]: {
  201. width: 'var(--size-menu, 4rem)',
  202. height: 'var(--size-menu, 4rem)',
  203. display: 'grid',
  204. placeContent: 'center',
  205. marginRight: 0,
  206. },
  207. })
  208. export const SidebarMenuContainer = styled('span')({
  209. boxSizing: 'border-box',
  210. display: 'grid',
  211. placeContent: 'center',
  212. width: '100%',
  213. textAlign: 'center',
  214. [minWidthFactor(3)]: {
  215. display: 'flex',
  216. justifyContent: 'flex-start',
  217. alignItems: 'center',
  218. width: `${configVar('base-width')}`,
  219. marginLeft: 'auto',
  220. paddingRight: '1rem',
  221. textAlign: 'left',
  222. boxSizing: 'border-box',
  223. },
  224. })
  225. export const MoreSidebarMenuContainer = styled('div')({
  226. display: 'flex',
  227. justifyContent: 'flex-start',
  228. alignItems: 'center',
  229. width: `calc(${configVar('base-width')} * 2)`,
  230. margin: '0 auto',
  231. padding: '0 1rem',
  232. textAlign: 'left',
  233. boxSizing: 'border-box',
  234. [minWidthFactor(3)]: {
  235. marginRight: 0,
  236. width: `${configVar('base-width')}`,
  237. paddingLeft: 0,
  238. },
  239. })
  240. export const ContentContainer = styled('div')({
  241. padding: '0 1rem',
  242. boxSizing: 'border-box',
  243. width: '100%',
  244. maxWidth: `calc(${configVar('base-width')} * 2)`,
  245. marginRight: 'auto',
  246. marginLeft: 'auto',
  247. [minWidthFactor(3)]: {
  248. marginLeft: 0,
  249. },
  250. })
  251. export const SidebarMainContainer = styled('div')({
  252. padding: '0 1rem',
  253. boxSizing: 'border-box',
  254. width: '100%',
  255. maxWidth: `calc(${configVar('base-width')} * 2)`,
  256. margin: '0 auto',
  257. [minWidthFactor(3)]: {
  258. maxWidth: 'none',
  259. },
  260. })
  261. type BaseMenuItem = {
  262. label: React.ReactChild,
  263. icon: React.ReactChild,
  264. url: unknown,
  265. }
  266. export type MenuItem = BaseMenuItem & {
  267. id: string,
  268. secondary?: boolean,
  269. }
  270. type Props = {
  271. brand?: React.ReactNode,
  272. sidebarMain?: React.ReactChild,
  273. sidebarMainOpen?: boolean,
  274. sidebarMenuItems: MenuItem[],
  275. moreItemsOpen?: boolean,
  276. moreLinkMenuItem: BaseMenuItem,
  277. menuLink?: React.ReactNode,
  278. userLink?: React.ReactNode,
  279. moreLinkComponent: React.ElementType,
  280. linkComponent: React.ElementType,
  281. topBarCenter?: React.ReactChild,
  282. }
  283. export const Layout: React.FC<Props> = ({
  284. brand,
  285. sidebarMain,
  286. sidebarMainOpen,
  287. sidebarMenuItems,
  288. moreItemsOpen,
  289. moreLinkMenuItem,
  290. menuLink,
  291. userLink,
  292. moreLinkComponent: MoreLinkComponent,
  293. linkComponent: LinkComponent,
  294. topBarCenter,
  295. children,
  296. }) => {
  297. const SidebarMainComponent = sidebarMainOpen ? OpenSidebarMain : SidebarMain
  298. const MoreItemsComponent = moreItemsOpen ? OpenMoreItems : MoreItems
  299. const primarySidebarMenuItems = sidebarMenuItems.filter(s => !s.secondary)
  300. const secondarySidebarMenuItems = sidebarMenuItems.filter(s => s.secondary)
  301. const visibleSecondarySidebarMenuItems = secondarySidebarMenuItems.slice(0, 1)
  302. const moreSecondarySidebarMenuItems = secondarySidebarMenuItems.slice(1)
  303. const visiblePrimarySidebarMenuItems = (
  304. visibleSecondarySidebarMenuItems.length === 1
  305. ? primarySidebarMenuItems.slice(0, 3)
  306. : primarySidebarMenuItems.slice(0, 4)
  307. )
  308. const morePrimarySidebarMenuItems = (
  309. visibleSecondarySidebarMenuItems.length === 1
  310. ? primarySidebarMenuItems.slice(3)
  311. : primarySidebarMenuItems.slice(4)
  312. )
  313. return (
  314. <>
  315. <Config />
  316. {
  317. (sidebarMainOpen || moreItemsOpen)
  318. && (
  319. <DisableScrolling />
  320. )
  321. }
  322. <>
  323. {
  324. (
  325. brand
  326. || userLink
  327. || topBarCenter
  328. || sidebarMain
  329. )
  330. && (
  331. <TopBar
  332. span="wide"
  333. brand={brand}
  334. menuLink={sidebarMain ? menuLink : undefined}
  335. userLink={userLink}
  336. >
  337. {topBarCenter}
  338. </TopBar>
  339. )
  340. }
  341. <SidebarBase>
  342. <SidebarMenu>
  343. <SidebarMenuSize>
  344. <SidebarMenuGroup>
  345. {visiblePrimarySidebarMenuItems.map((item) => {
  346. return (
  347. <SidebarMenuItem
  348. key={item.id}
  349. >
  350. <LinkComponent
  351. {...item}
  352. />
  353. </SidebarMenuItem>
  354. )
  355. })}
  356. </SidebarMenuGroup>
  357. <MoreItemsComponent>
  358. <MoreItemsScroll>
  359. <MorePrimarySidebarMenuGroup>
  360. {morePrimarySidebarMenuItems.map((item) => {
  361. return (
  362. <MoreSidebarMenuItem
  363. key={item.id}
  364. >
  365. <MoreLinkComponent
  366. {...item}
  367. />
  368. </MoreSidebarMenuItem>
  369. )
  370. })}
  371. </MorePrimarySidebarMenuGroup>
  372. <MoreSecondarySidebarMenuGroup>
  373. {moreSecondarySidebarMenuItems.map((item) => {
  374. return (
  375. <MoreSidebarMenuItem
  376. key={item.id}
  377. >
  378. <MoreLinkComponent
  379. {...item}
  380. />
  381. </MoreSidebarMenuItem>
  382. )
  383. })}
  384. </MoreSecondarySidebarMenuGroup>
  385. </MoreItemsScroll>
  386. </MoreItemsComponent>
  387. {
  388. (
  389. morePrimarySidebarMenuItems.length > 0
  390. || moreSecondarySidebarMenuItems.length > 0
  391. )
  392. && (
  393. <MoreToggleSidebarMenuItem>
  394. <SidebarMenuItem>
  395. <LinkComponent
  396. {...moreLinkMenuItem}
  397. />
  398. </SidebarMenuItem>
  399. </MoreToggleSidebarMenuItem>
  400. )
  401. }
  402. {
  403. visibleSecondarySidebarMenuItems.length > 0
  404. && (
  405. <SidebarMenuGroup>
  406. {visibleSecondarySidebarMenuItems.map((item) => (
  407. <SidebarMenuItem
  408. key={item.id}
  409. >
  410. <LinkComponent
  411. {...item}
  412. />
  413. </SidebarMenuItem>
  414. ))}
  415. </SidebarMenuGroup>
  416. )
  417. }
  418. </SidebarMenuSize>
  419. </SidebarMenu>
  420. {
  421. (sidebarMain as unknown)
  422. && (
  423. <SidebarMainComponent>
  424. {sidebarMain}
  425. </SidebarMainComponent>
  426. )
  427. }
  428. </SidebarBase>
  429. <ContentBase>
  430. {children}
  431. </ContentBase>
  432. </>
  433. </>
  434. )
  435. }