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.
 
 
 

456 lines
9.7 KiB

  1. import * as React from 'react';
  2. import styled, { createGlobalStyle } from 'styled-components'
  3. import TopBar from '../../widgets/TopBar'
  4. import {applyBackgroundColor, 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. ...applyBackgroundColor(),
  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. ...applyBackgroundColor(),
  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. ...applyBackgroundColor(),
  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. ...applyBackgroundColor(),
  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. <TopBar
  324. wide
  325. brand={brand}
  326. menuLink={sidebarMain ? menuLink : undefined}
  327. userLink={userLink}
  328. >
  329. {topBarCenter}
  330. </TopBar>
  331. <SidebarBase>
  332. <SidebarMenu>
  333. <SidebarMenuSize>
  334. <SidebarMenuGroup>
  335. {visiblePrimarySidebarMenuItems.map((item) => {
  336. return (
  337. <SidebarMenuItem
  338. key={item.id}
  339. >
  340. <LinkComponent
  341. {...item}
  342. />
  343. </SidebarMenuItem>
  344. )
  345. })}
  346. </SidebarMenuGroup>
  347. <MoreItemsComponent>
  348. <MoreItemsScroll>
  349. <MorePrimarySidebarMenuGroup>
  350. {morePrimarySidebarMenuItems.map((item) => {
  351. return (
  352. <MoreSidebarMenuItem
  353. key={item.id}
  354. >
  355. <MoreLinkComponent
  356. {...item}
  357. />
  358. </MoreSidebarMenuItem>
  359. )
  360. })}
  361. </MorePrimarySidebarMenuGroup>
  362. <MoreSecondarySidebarMenuGroup>
  363. {moreSecondarySidebarMenuItems.map((item) => {
  364. return (
  365. <MoreSidebarMenuItem
  366. key={item.id}
  367. >
  368. <MoreLinkComponent
  369. {...item}
  370. />
  371. </MoreSidebarMenuItem>
  372. )
  373. })}
  374. </MoreSecondarySidebarMenuGroup>
  375. </MoreItemsScroll>
  376. </MoreItemsComponent>
  377. {
  378. (
  379. morePrimarySidebarMenuItems.length > 0
  380. || moreSecondarySidebarMenuItems.length > 0
  381. )
  382. && (
  383. <MoreToggleSidebarMenuItem>
  384. <SidebarMenuItem>
  385. <LinkComponent
  386. {...moreLinkMenuItem}
  387. />
  388. </SidebarMenuItem>
  389. </MoreToggleSidebarMenuItem>
  390. )
  391. }
  392. {
  393. visibleSecondarySidebarMenuItems.length > 0
  394. && (
  395. <SidebarMenuGroup>
  396. {visibleSecondarySidebarMenuItems.map((item) => (
  397. <SidebarMenuItem
  398. key={item.id}
  399. >
  400. <LinkComponent
  401. {...item}
  402. />
  403. </SidebarMenuItem>
  404. ))}
  405. </SidebarMenuGroup>
  406. )
  407. }
  408. </SidebarMenuSize>
  409. </SidebarMenu>
  410. {
  411. (sidebarMain as unknown)
  412. && (
  413. <SidebarMainComponent>
  414. {sidebarMain}
  415. </SidebarMainComponent>
  416. )
  417. }
  418. </SidebarBase>
  419. <ContentBase>
  420. {children}
  421. </ContentBase>
  422. </>
  423. </>
  424. )
  425. }