Template for starting apps, powered by Next.js
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.
 
 

533 lines
12 KiB

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