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.
 
 
 

454 lines
10 KiB

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