Layout scaffolding for Web apps.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 

454 lignes
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. }