@@ -0,0 +1,10 @@ | |||||
root = true | |||||
[*] | |||||
charset = utf-8 | |||||
end_of_line = lf | |||||
indent_style = tab | |||||
insert_final_newline = true | |||||
max_line_length = 120 | |||||
tab_width = 2 | |||||
trim_trailing_whitespace = true |
@@ -0,0 +1,7 @@ | |||||
*.log | |||||
.DS_Store | |||||
node_modules | |||||
.cache | |||||
dist | |||||
.idea/ | |||||
example/ |
@@ -0,0 +1,21 @@ | |||||
MIT License | |||||
Copyright (c) 2021 TheoryOfNekomata | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights | |||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
copies of the Software, and to permit persons to whom the Software is | |||||
furnished to do so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in all | |||||
copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. |
@@ -0,0 +1,52 @@ | |||||
# Viewfinder | |||||
Layout scaffolding for Web apps. | |||||
## Usage | |||||
Just import: | |||||
```tsx | |||||
import * as React from 'react' | |||||
import { Basic, LeftSidebar, LeftSidebarWithMenu, RightSidebarStatic } from '@tesseract-design/viewfinder' | |||||
const Page: React.FC = ({ | |||||
avatar, | |||||
fullName, | |||||
}) => ( | |||||
<Basic.Layout | |||||
brand={ | |||||
<a href="/"> | |||||
<img | |||||
src="logo.svg" | |||||
alt="ACME Inc." | |||||
/> | |||||
</a> | |||||
} | |||||
topBarCenter={ | |||||
<form> | |||||
<input | |||||
type="search" | |||||
name="q" | |||||
/> | |||||
</form> | |||||
} | |||||
userLink={ | |||||
<a href="/profile"> | |||||
<img | |||||
src={avatar} | |||||
alt={fullName} | |||||
/> | |||||
</a> | |||||
} | |||||
> | |||||
<Basic.ContentContainer> | |||||
Hello world! | |||||
</Basic.ContentContainer> | |||||
</Basic.Layout> | |||||
) | |||||
export default Page | |||||
``` | |||||
The available props per layout is included as a TypeScript declarations file. |
@@ -0,0 +1,59 @@ | |||||
{ | |||||
"version": "0.1.0", | |||||
"license": "MIT", | |||||
"main": "dist/index.js", | |||||
"typings": "dist/index.d.ts", | |||||
"files": [ | |||||
"dist", | |||||
"src" | |||||
], | |||||
"engines": { | |||||
"node": ">=10" | |||||
}, | |||||
"scripts": { | |||||
"start": "tsdx watch", | |||||
"build": "tsdx build", | |||||
"test": "tsdx test --passWithNoTests", | |||||
"lint": "tsdx lint", | |||||
"prepare": "tsdx build", | |||||
"size": "size-limit", | |||||
"analyze": "size-limit --why" | |||||
}, | |||||
"peerDependencies": { | |||||
"react": ">=16", | |||||
"styled-components": "~5.2.3" | |||||
}, | |||||
"husky": { | |||||
"hooks": { | |||||
"pre-commit": "tsdx lint" | |||||
} | |||||
}, | |||||
"name": "@tesseract-design/viewfinder", | |||||
"description": "Layout scaffolding for Web apps.", | |||||
"author": "TheoryOfNekomata", | |||||
"module": "dist/starter.esm.js", | |||||
"size-limit": [ | |||||
{ | |||||
"path": "dist/starter.cjs.production.min.js", | |||||
"limit": "10 KB" | |||||
}, | |||||
{ | |||||
"path": "dist/starter.esm.js", | |||||
"limit": "10 KB" | |||||
} | |||||
], | |||||
"devDependencies": { | |||||
"@size-limit/preset-small-lib": "^4.10.2", | |||||
"@types/react": "^17.0.3", | |||||
"@types/react-dom": "^17.0.3", | |||||
"@types/styled-components": "^5.1.9", | |||||
"husky": "^6.0.0", | |||||
"react": "^17.0.2", | |||||
"react-dom": "^17.0.2", | |||||
"size-limit": "^4.10.2", | |||||
"tsdx": "^0.14.1", | |||||
"tslib": "^2.2.0", | |||||
"typescript": "^4.2.4", | |||||
"styled-components": "^5.2.3" | |||||
} | |||||
} |
@@ -0,0 +1,4 @@ | |||||
export * as Basic from './layouts/Basic' | |||||
export * as LeftSidebarWithMenu from './layouts/LeftSidebarWithMenu' | |||||
export * as LeftSidebar from './layouts/LeftSidebar' | |||||
export * as RightSidebarStatic from './layouts/RightSidebarStatic' |
@@ -0,0 +1,47 @@ | |||||
import * as React from 'react' | |||||
import styled from 'styled-components'; | |||||
import TopBar from '../../widgets/TopBar'; | |||||
const LayoutBase = styled('div')({ | |||||
'--width-base': 'var(--width-base, 360px)', | |||||
'--height-topbar': 'var(--height-topbar, 4rem)', | |||||
}) | |||||
const ContentBase = styled('main')({ | |||||
boxSizing: 'border-box', | |||||
}) | |||||
export const ContentContainer = styled('div')({ | |||||
padding: '0 1rem', | |||||
boxSizing: 'border-box', | |||||
margin: '0 auto', | |||||
maxWidth: 'calc(var(--width-base, 360px) * 2)', | |||||
width: '100%', | |||||
}) | |||||
type Props = { | |||||
brand?: React.ReactNode, | |||||
userLink?: React.ReactNode, | |||||
topBarCenter?: React.ReactChild, | |||||
} | |||||
export const Layout: React.FC<Props> = ({ | |||||
brand, | |||||
userLink, | |||||
topBarCenter, | |||||
children, | |||||
}) => { | |||||
return ( | |||||
<LayoutBase> | |||||
<TopBar | |||||
brand={brand} | |||||
userLink={userLink} | |||||
> | |||||
{topBarCenter} | |||||
</TopBar> | |||||
<ContentBase> | |||||
{children} | |||||
</ContentBase> | |||||
</LayoutBase> | |||||
) | |||||
} |
@@ -0,0 +1,129 @@ | |||||
import * as React from 'react'; | |||||
import styled, {createGlobalStyle} from 'styled-components'; | |||||
import TopBar from '../../widgets/TopBar'; | |||||
const DisableScrolling = createGlobalStyle({ | |||||
'body': { | |||||
overflow: 'hidden', | |||||
'@media (min-width: 1080px)': { | |||||
overflow: 'auto', | |||||
}, | |||||
}, | |||||
}) | |||||
const LayoutBase = styled('div')({ | |||||
'--width-base': 'var(--width-base, 360px)', | |||||
'--height-topbar': 'var(--height-topbar, 4rem)', | |||||
}) | |||||
const ContentBase = styled('main')({ | |||||
boxSizing: 'border-box', | |||||
'@media (min-width: 1080px)': { | |||||
paddingLeft: 'calc(50% - var(--width-base, 360px) * 0.5)', | |||||
}, | |||||
}) | |||||
const SidebarOverflow = styled('div')({ | |||||
width: '100%', | |||||
height: '100%', | |||||
overflow: 'auto', | |||||
// overflow: 'overlay', | |||||
scrollbarWidth: 'none', | |||||
'::-webkit-scrollbar': { | |||||
display: 'none', | |||||
}, | |||||
}) | |||||
const SidebarBase = styled('div')({ | |||||
boxSizing: 'border-box', | |||||
position: 'fixed', | |||||
top: 0, | |||||
left: '-100%', | |||||
width: '100%', | |||||
height: '100%', | |||||
backgroundColor: 'var(--color-bg, white)', | |||||
overflow: 'hidden', | |||||
'@media (prefers-color-scheme: dark)': { | |||||
backgroundColor: 'var(--color-bg, black)', | |||||
}, | |||||
'@media (min-width: 1080px)': { | |||||
width: 'calc(50% - var(--width-base, 360px) * 0.5)', | |||||
left: 0, | |||||
}, | |||||
}) | |||||
const OpenSidebarBase = styled(SidebarBase)({ | |||||
left: 0, | |||||
}) | |||||
export const SidebarContainer = styled('div')({ | |||||
margin: '0 auto', | |||||
padding: '0 1rem', | |||||
boxSizing: 'border-box', | |||||
maxWidth: 'calc(var(--width-base, 360px) * 2)', | |||||
'@media (min-width: 1080px)': { | |||||
width: 'var(--width-base, 360px)', | |||||
marginRight: 0, | |||||
}, | |||||
}) | |||||
export const ContentContainer = styled('div')({ | |||||
padding: '0 1rem', | |||||
boxSizing: 'border-box', | |||||
width: '100%', | |||||
maxWidth: 'calc(var(--width-base, 360px) * 2)', | |||||
marginRight: 'auto', | |||||
marginLeft: 'auto', | |||||
'@media (min-width: 1080px)': { | |||||
marginLeft: 0, | |||||
}, | |||||
}) | |||||
type Props = { | |||||
brand?: React.ReactNode, | |||||
sidebarMain?: React.ReactChild, | |||||
sidebarMainOpen?: boolean, | |||||
menuLink?: React.ReactNode, | |||||
userLink?: React.ReactNode, | |||||
topBarCenter?: React.ReactChild, | |||||
} | |||||
export const Layout: React.FC<Props> = ({ | |||||
brand, | |||||
sidebarMain, | |||||
sidebarMainOpen, | |||||
menuLink, | |||||
userLink, | |||||
topBarCenter, | |||||
children, | |||||
}) => { | |||||
const LeftSidebarComponent = sidebarMainOpen ? OpenSidebarBase : SidebarBase | |||||
return ( | |||||
<LayoutBase> | |||||
{ | |||||
sidebarMainOpen | |||||
&& ( | |||||
<DisableScrolling /> | |||||
) | |||||
} | |||||
<TopBar | |||||
wide | |||||
brand={brand} | |||||
menuLink={menuLink} | |||||
userLink={userLink} | |||||
> | |||||
{topBarCenter} | |||||
</TopBar> | |||||
<LeftSidebarComponent> | |||||
<SidebarOverflow> | |||||
{sidebarMain} | |||||
</SidebarOverflow> | |||||
</LeftSidebarComponent> | |||||
<ContentBase> | |||||
{children} | |||||
</ContentBase> | |||||
</LayoutBase> | |||||
) | |||||
} |
@@ -0,0 +1,453 @@ | |||||
import * as React from 'react'; | |||||
import styled, { createGlobalStyle } from 'styled-components'; | |||||
import TopBar from '../../widgets/TopBar'; | |||||
const DisableScrolling = createGlobalStyle({ | |||||
'body': { | |||||
overflow: 'hidden', | |||||
'@media (min-width: 1080px)': { | |||||
overflow: 'auto', | |||||
}, | |||||
}, | |||||
}) | |||||
const LayoutBase = styled('div')({ | |||||
'--width-base': 'var(--width-base, 360px)', | |||||
'--height-topbar': 'var(--height-topbar, 4rem)', | |||||
'--size-menu': 'var(--size-menu, 4rem)', | |||||
}) | |||||
const ContentBase = styled('main')({ | |||||
boxSizing: 'border-box', | |||||
paddingBottom: 'var(--size-menu, 4rem)', | |||||
'@media (min-width: 1080px)': { | |||||
paddingLeft: 'calc(50% - var(--width-base, 360px) * 0.5)', | |||||
paddingBottom: 0, | |||||
}, | |||||
}) | |||||
const SidebarBase = styled('div')({ | |||||
boxSizing: 'border-box', | |||||
backgroundColor: 'var(--color-bg, white)', | |||||
overflow: 'hidden', | |||||
display: 'contents', | |||||
left: 'calc(var(--width-base, 360px) * -1)', | |||||
'@media (prefers-color-scheme: dark)': { | |||||
backgroundColor: 'var(--color-bg, black)', | |||||
}, | |||||
'@media (min-width: 1080px)': { | |||||
position: 'fixed', | |||||
top: 0, | |||||
left: 0, | |||||
width: 'calc(50% - var(--width-base, 360px) * 0.5)', | |||||
height: '100%', | |||||
display: 'block', | |||||
}, | |||||
}) | |||||
const SidebarMain = styled('div')({ | |||||
backgroundColor: 'var(--color-bg, white)', | |||||
boxSizing: 'border-box', | |||||
position: 'fixed', | |||||
top: 0, | |||||
right: '100%', | |||||
width: '100%', | |||||
height: '100%', | |||||
overflow: 'auto', | |||||
// overflow: 'overlay', | |||||
paddingTop: 'inherit', | |||||
paddingBottom: 'var(--size-menu, 4rem)', | |||||
'@media (prefers-color-scheme: dark)': { | |||||
backgroundColor: 'var(--color-bg, black)', | |||||
}, | |||||
scrollbarWidth: 'none', | |||||
'::-webkit-scrollbar': { | |||||
display: 'none', | |||||
}, | |||||
'@media (min-width: 1080px)': { | |||||
position: 'absolute', | |||||
right: 0, | |||||
width: 'calc(var(--width-base, 360px) - var(--size-menu, 4rem))', | |||||
marginLeft: 'auto', | |||||
paddingBottom: 0, | |||||
}, | |||||
}) | |||||
const OpenSidebarMain = styled(SidebarMain)({ | |||||
right: 0, | |||||
}) | |||||
const SidebarMenu = styled('div')({ | |||||
boxSizing: 'border-box', | |||||
overflow: 'auto', | |||||
// overflow: 'overlay', | |||||
scrollbarWidth: 'none', | |||||
'::-webkit-scrollbar': { | |||||
display: 'none', | |||||
}, | |||||
position: 'fixed', | |||||
bottom: 0, | |||||
left: 0, | |||||
width: '100%', | |||||
height: 'var(--size-menu, 4rem)', | |||||
zIndex: 1, | |||||
backgroundColor: 'var(--color-bg, white)', | |||||
'@media (prefers-color-scheme: dark)': { | |||||
backgroundColor: 'var(--color-bg, black)', | |||||
}, | |||||
'@media (min-width: 1080px)': { | |||||
top: 0, | |||||
marginLeft: 'auto', | |||||
position: 'absolute', | |||||
height: '100%', | |||||
paddingTop: 'inherit', | |||||
overflow: 'auto', | |||||
zIndex: 'auto', | |||||
}, | |||||
}) | |||||
const SidebarMenuSize = styled('div')({ | |||||
display: 'flex', | |||||
width: '100%', | |||||
height: '100%', | |||||
maxWidth: 'calc(var(--width-base, 360px) * 2)', | |||||
margin: '0 auto', | |||||
'@media (min-width: 1080px)': { | |||||
maxWidth: 'none', | |||||
marginRight: 0, | |||||
flexDirection: 'column', | |||||
justifyContent: 'space-between', | |||||
alignItems: 'flex-end', | |||||
}, | |||||
}) | |||||
const SidebarMenuGroup = styled('div')({ | |||||
display: 'contents', | |||||
'@media (min-width: 1080px)': { | |||||
width: '100%', | |||||
display: 'block', | |||||
}, | |||||
}) | |||||
const MoreItems = styled('div')({ | |||||
position: 'fixed', | |||||
top: 0, | |||||
left: '-100%', | |||||
width: '100%', | |||||
height: '100%', | |||||
paddingTop: 'var(--height-topbar, 4rem)', | |||||
paddingBottom: 'var(--size-menu, 4rem)', | |||||
backgroundColor: 'var(--color-bg, white)', | |||||
zIndex: -1, | |||||
boxSizing: 'border-box', | |||||
'@media (prefers-color-scheme: dark)': { | |||||
backgroundColor: 'var(--color-bg, black)', | |||||
}, | |||||
'@media (min-width: 1080px)': { | |||||
display: 'contents', | |||||
}, | |||||
}) | |||||
const OpenMoreItems = styled(MoreItems)({ | |||||
left: 0, | |||||
}) | |||||
const MoreItemsScroll = styled('div')({ | |||||
width: '100%', | |||||
height: '100%', | |||||
overflow: 'auto', | |||||
'@media (min-width: 1080px)': { | |||||
display: 'contents', | |||||
}, | |||||
}) | |||||
const MorePrimarySidebarMenuGroup = styled(SidebarMenuGroup)({ | |||||
'@media (min-width: 1080px)': { | |||||
flex: 'auto', | |||||
}, | |||||
}) | |||||
const MoreSecondarySidebarMenuGroup = styled(SidebarMenuGroup)({ | |||||
'@media (min-width: 1080px)': { | |||||
order: 4, | |||||
}, | |||||
}) | |||||
const SidebarMenuItem = styled('span')({ | |||||
width: 0, | |||||
flex: 'auto', | |||||
height: 'var(--size-menu, 4rem)', | |||||
'> *': { | |||||
height: '100%', | |||||
display: 'flex', | |||||
alignItems: 'center', | |||||
textDecoration: 'none', | |||||
width: '100%', | |||||
}, | |||||
'@media (min-width: 1080px)': { | |||||
width: 'auto !important', | |||||
flex: '0 1 auto', | |||||
'> *': { | |||||
height: 'auto', | |||||
} | |||||
}, | |||||
}) | |||||
const MoreSidebarMenuItem = styled('span')({ | |||||
display: 'block', | |||||
height: 'var(--size-menu, 4rem)', | |||||
'> *': { | |||||
height: '100%', | |||||
display: 'flex', | |||||
alignItems: 'center', | |||||
textDecoration: 'none', | |||||
width: '100%', | |||||
}, | |||||
'@media (min-width: 1080px)': { | |||||
width: 'auto !important', | |||||
flex: '0 1 auto', | |||||
}, | |||||
}) | |||||
const MoreToggleSidebarMenuItem = styled(SidebarMenuItem)({ | |||||
'@media (min-width: 1080px)': { | |||||
display: 'none', | |||||
}, | |||||
}) | |||||
export const SidebarMenuItemIcon = styled('span')({ | |||||
display: 'block', | |||||
'@media (min-width: 1080px)': { | |||||
width: 'var(--size-menu, 4rem)', | |||||
height: 'var(--size-menu, 4rem)', | |||||
display: 'grid', | |||||
placeContent: 'center', | |||||
}, | |||||
}) | |||||
export const MoreSidebarMenuItemIcon = styled('span')({ | |||||
marginRight: '1rem', | |||||
display: 'block', | |||||
'@media (min-width: 1080px)': { | |||||
width: 'var(--size-menu, 4rem)', | |||||
height: 'var(--size-menu, 4rem)', | |||||
display: 'grid', | |||||
placeContent: 'center', | |||||
marginRight: 0, | |||||
}, | |||||
}) | |||||
export const SidebarMenuContainer = styled('span')({ | |||||
boxSizing: 'border-box', | |||||
display: 'grid', | |||||
placeContent: 'center', | |||||
width: '100%', | |||||
textAlign: 'center', | |||||
'@media (min-width: 1080px)': { | |||||
display: 'flex', | |||||
justifyContent: 'flex-start', | |||||
alignItems: 'center', | |||||
width: 'var(--width-base, 360px)', | |||||
marginLeft: 'auto', | |||||
paddingRight: '1rem', | |||||
textAlign: 'left', | |||||
boxSizing: 'border-box', | |||||
}, | |||||
}) | |||||
export const MoreSidebarMenuContainer = styled('div')({ | |||||
display: 'flex', | |||||
justifyContent: 'flex-start', | |||||
alignItems: 'center', | |||||
width: 'calc(var(--width-base, 360px) * 2)', | |||||
margin: '0 auto', | |||||
padding: '0 1rem', | |||||
textAlign: 'left', | |||||
boxSizing: 'border-box', | |||||
'@media (min-width: 1080px)': { | |||||
marginRight: 0, | |||||
width: 'var(--width-base, 360px)', | |||||
paddingLeft: 0, | |||||
}, | |||||
}) | |||||
export const ContentContainer = styled('div')({ | |||||
padding: '0 1rem', | |||||
boxSizing: 'border-box', | |||||
width: '100%', | |||||
maxWidth: 'calc(var(--width-base, 360px) * 2)', | |||||
marginRight: 'auto', | |||||
marginLeft: 'auto', | |||||
'@media (min-width: 1080px)': { | |||||
marginLeft: 0, | |||||
}, | |||||
}) | |||||
export const SidebarMainContainer = styled('div')({ | |||||
padding: '0 1rem', | |||||
boxSizing: 'border-box', | |||||
width: '100%', | |||||
maxWidth: 'calc(var(--width-base, 360px) * 2)', | |||||
margin: '0 auto', | |||||
'@media (min-width: 1080px)': { | |||||
maxWidth: 'none', | |||||
}, | |||||
}) | |||||
type BaseMenuItem = { | |||||
label: React.ReactChild, | |||||
icon: React.ReactChild, | |||||
url: unknown, | |||||
} | |||||
export type MenuItem = BaseMenuItem & { | |||||
id: string, | |||||
secondary?: boolean, | |||||
} | |||||
type Props = { | |||||
brand?: React.ReactNode, | |||||
sidebarMain: React.ReactChild, | |||||
sidebarMainOpen?: boolean, | |||||
sidebarMenuItems: MenuItem[], | |||||
moreItemsOpen?: boolean, | |||||
moreLinkMenuItem: BaseMenuItem, | |||||
menuLink?: React.ReactNode, | |||||
userLink?: React.ReactNode, | |||||
moreLinkComponent: React.ElementType, | |||||
linkComponent: React.ElementType, | |||||
topBarCenter?: React.ReactChild, | |||||
} | |||||
export const Layout: React.FC<Props> = ({ | |||||
brand, | |||||
sidebarMain, | |||||
sidebarMainOpen, | |||||
sidebarMenuItems, | |||||
moreItemsOpen, | |||||
moreLinkMenuItem, | |||||
menuLink, | |||||
userLink, | |||||
moreLinkComponent: MoreLinkComponent, | |||||
linkComponent: LinkComponent, | |||||
topBarCenter, | |||||
children, | |||||
}) => { | |||||
const SidebarMainComponent = sidebarMainOpen ? OpenSidebarMain : SidebarMain | |||||
const MoreItemsComponent = moreItemsOpen ? OpenMoreItems : MoreItems | |||||
const primarySidebarMenuItems = sidebarMenuItems.filter(s => !s.secondary) | |||||
const secondarySidebarMenuItems = sidebarMenuItems.filter(s => s.secondary) | |||||
const visibleSecondarySidebarMenuItems = secondarySidebarMenuItems.slice(0, 1) | |||||
const moreSecondarySidebarMenuItems = secondarySidebarMenuItems.slice(1) | |||||
const visiblePrimarySidebarMenuItems = ( | |||||
visibleSecondarySidebarMenuItems.length === 1 | |||||
? primarySidebarMenuItems.slice(0, 3) | |||||
: primarySidebarMenuItems.slice(0, 4) | |||||
) | |||||
const morePrimarySidebarMenuItems = ( | |||||
visibleSecondarySidebarMenuItems.length === 1 | |||||
? primarySidebarMenuItems.slice(3) | |||||
: primarySidebarMenuItems.slice(4) | |||||
) | |||||
return ( | |||||
<> | |||||
{ | |||||
(sidebarMainOpen || moreItemsOpen) | |||||
&& ( | |||||
<DisableScrolling /> | |||||
) | |||||
} | |||||
<LayoutBase> | |||||
<TopBar | |||||
wide | |||||
brand={brand} | |||||
menuLink={menuLink} | |||||
userLink={userLink} | |||||
> | |||||
{topBarCenter} | |||||
</TopBar> | |||||
<SidebarBase> | |||||
<SidebarMenu> | |||||
<SidebarMenuSize> | |||||
<SidebarMenuGroup> | |||||
{visiblePrimarySidebarMenuItems.map((item) => { | |||||
return ( | |||||
<SidebarMenuItem | |||||
key={item.id} | |||||
> | |||||
<LinkComponent | |||||
{...item} | |||||
/> | |||||
</SidebarMenuItem> | |||||
) | |||||
})} | |||||
</SidebarMenuGroup> | |||||
<MoreItemsComponent> | |||||
<MoreItemsScroll> | |||||
<MorePrimarySidebarMenuGroup> | |||||
{morePrimarySidebarMenuItems.map((item) => { | |||||
return ( | |||||
<MoreSidebarMenuItem | |||||
key={item.id} | |||||
> | |||||
<MoreLinkComponent | |||||
{...item} | |||||
/> | |||||
</MoreSidebarMenuItem> | |||||
) | |||||
})} | |||||
</MorePrimarySidebarMenuGroup> | |||||
<MoreSecondarySidebarMenuGroup> | |||||
{moreSecondarySidebarMenuItems.map((item) => { | |||||
return ( | |||||
<MoreSidebarMenuItem | |||||
key={item.id} | |||||
> | |||||
<MoreLinkComponent | |||||
{...item} | |||||
/> | |||||
</MoreSidebarMenuItem> | |||||
) | |||||
})} | |||||
</MoreSecondarySidebarMenuGroup> | |||||
</MoreItemsScroll> | |||||
</MoreItemsComponent> | |||||
<MoreToggleSidebarMenuItem> | |||||
<SidebarMenuItem> | |||||
<LinkComponent | |||||
{...moreLinkMenuItem} | |||||
/> | |||||
</SidebarMenuItem> | |||||
</MoreToggleSidebarMenuItem> | |||||
{ | |||||
visibleSecondarySidebarMenuItems.length > 0 | |||||
&& ( | |||||
<SidebarMenuGroup> | |||||
{visibleSecondarySidebarMenuItems.map((item) => ( | |||||
<SidebarMenuItem | |||||
key={item.id} | |||||
> | |||||
<LinkComponent | |||||
{...item} | |||||
/> | |||||
</SidebarMenuItem> | |||||
))} | |||||
</SidebarMenuGroup> | |||||
) | |||||
} | |||||
</SidebarMenuSize> | |||||
</SidebarMenu> | |||||
<SidebarMainComponent> | |||||
{sidebarMain} | |||||
</SidebarMainComponent> | |||||
</SidebarBase> | |||||
<ContentBase> | |||||
{children} | |||||
</ContentBase> | |||||
</LayoutBase> | |||||
</> | |||||
) | |||||
} |
@@ -0,0 +1,94 @@ | |||||
import * as React from 'react'; | |||||
import styled from 'styled-components'; | |||||
import TopBar from '../../widgets/TopBar'; | |||||
const LayoutBase = styled('div')({ | |||||
'--width-base': 'var(--width-base, 360px)', | |||||
'--height-topbar': 'var(--height-topbar, 4rem)', | |||||
}) | |||||
const ContentBase = styled('main')({ | |||||
boxSizing: 'border-box', | |||||
'@media (min-width: 1080px)': { | |||||
paddingRight: 'calc(50% - var(--width-base, 360px) * 0.5)', | |||||
}, | |||||
}) | |||||
const SidebarBase = styled('div')({ | |||||
boxSizing: 'border-box', | |||||
backgroundColor: 'var(--color-bg, white)', | |||||
// prevent collapse of margin | |||||
'::after': { | |||||
content: "''", | |||||
display: 'block', | |||||
paddingBottom: 1, | |||||
marginTop: -1, | |||||
boxSizing: 'border-box', | |||||
}, | |||||
'@media (prefers-color-scheme: dark)': { | |||||
backgroundColor: 'var(--color-bg, black)', | |||||
}, | |||||
'@media (min-width: 1080px)': { | |||||
position: 'absolute', | |||||
top: 0, | |||||
right: 0, | |||||
width: 'calc(50% - var(--width-base, 360px) * 0.5)', | |||||
height: '100%', | |||||
}, | |||||
}) | |||||
export const SidebarContainer = styled('div')({ | |||||
padding: '0 1rem', | |||||
boxSizing: 'border-box', | |||||
width: '100%', | |||||
maxWidth: 'calc(var(--width-base, 360px) * 2)', | |||||
margin: '0 auto', | |||||
'@media (min-width: 1080px)': { | |||||
width: 'var(--width-base, 360px)', | |||||
marginLeft: 0, | |||||
}, | |||||
}) | |||||
export const ContentContainer = styled('div')({ | |||||
padding: '0 1rem', | |||||
boxSizing: 'border-box', | |||||
width: '100%', | |||||
maxWidth: 'calc(var(--width-base, 360px) * 2)', | |||||
margin: '0 auto', | |||||
'@media (min-width: 1080px)': { | |||||
marginRight: 0, | |||||
}, | |||||
}) | |||||
type Props = { | |||||
brand?: React.ReactNode, | |||||
sidebarMain: React.ReactChild, | |||||
userLink?: React.ReactNode, | |||||
topBarCenter?: React.ReactChild, | |||||
} | |||||
export const Layout: React.FC<Props> = ({ | |||||
brand, | |||||
sidebarMain, | |||||
userLink, | |||||
topBarCenter, | |||||
children, | |||||
}) => { | |||||
return ( | |||||
<LayoutBase> | |||||
<TopBar | |||||
wide | |||||
brand={brand} | |||||
userLink={userLink} | |||||
> | |||||
{topBarCenter} | |||||
</TopBar> | |||||
<ContentBase> | |||||
{children} | |||||
</ContentBase> | |||||
<SidebarBase> | |||||
{sidebarMain} | |||||
</SidebarBase> | |||||
</LayoutBase> | |||||
) | |||||
} |
@@ -0,0 +1,138 @@ | |||||
import * as React from 'react' | |||||
import styled from 'styled-components' | |||||
const Base = styled('div')({ | |||||
position: 'fixed', | |||||
top: 0, | |||||
left: 0, | |||||
width: '100%', | |||||
height: 'var(--height-topbar, 4rem)', | |||||
backgroundColor: 'var(--color-bg, white)', | |||||
zIndex: 2, | |||||
'@media (prefers-color-scheme: dark)': { | |||||
backgroundColor: 'var(--color-bg, black)', | |||||
}, | |||||
'~ *': { | |||||
paddingTop: 'var(--height-topbar, 4rem)', | |||||
}, | |||||
'~ main ~ *': { | |||||
paddingTop: 0, | |||||
}, | |||||
'@media (min-width: 1080px)': { | |||||
'~ main ~ *': { | |||||
paddingTop: 'var(--height-topbar, 4rem)', | |||||
}, | |||||
}, | |||||
}) | |||||
const Container = styled('div')({ | |||||
padding: '0 1rem', | |||||
boxSizing: 'border-box', | |||||
margin: '0 auto', | |||||
maxWidth: 'calc(var(--width-base, 360px) * 2)', | |||||
width: '100%', | |||||
height: '100%', | |||||
display: 'flex', | |||||
alignItems: 'center', | |||||
}) | |||||
const WideContainer = styled(Container)({ | |||||
'@media (min-width: 1080px)': { | |||||
maxWidth: 'calc(var(--width-base, 360px) * 3)', | |||||
}, | |||||
}) | |||||
const BrandContainer = styled('div')({ | |||||
}) | |||||
const CenterContainer = styled('div')({ | |||||
flex: 'auto', | |||||
padding: '0 1rem', | |||||
boxSizing: 'border-box', | |||||
':first-child': { | |||||
paddingLeft: 0, | |||||
}, | |||||
}) | |||||
const ActionContainer = styled('div')({ | |||||
display: 'flex', | |||||
alignItems: 'center', | |||||
justifyContent: 'flex-end', | |||||
height: '100%', | |||||
whiteSpace: 'nowrap', | |||||
'@media (min-width: 720px)': { | |||||
minWidth: '8rem', | |||||
}, | |||||
}) | |||||
const LinkContainer = styled('div')({ | |||||
width: 'var(--height-topbar, 4rem)', | |||||
height: '100%', | |||||
'> *': { | |||||
width: '100%', | |||||
height: '100%', | |||||
display: 'inline-grid', | |||||
placeContent: 'center', | |||||
}, | |||||
}) | |||||
const MenuLinkContainer = styled(LinkContainer)({ | |||||
'@media (min-width: 1080px)': { | |||||
position: 'absolute', | |||||
left: -999999, | |||||
}, | |||||
}) | |||||
type Props = { | |||||
wide?: boolean, | |||||
brand?: React.ReactNode, | |||||
menuLink?: React.ReactNode, | |||||
userLink?: React.ReactNode, | |||||
} | |||||
const TopBar: React.FC<Props> = ({ | |||||
wide, | |||||
brand, | |||||
menuLink, | |||||
userLink, | |||||
children, | |||||
}) => { | |||||
const ContainerComponent = wide ? WideContainer : Container | |||||
return ( | |||||
<Base> | |||||
<ContainerComponent> | |||||
{ | |||||
Boolean(brand as unknown) | |||||
&& ( | |||||
<BrandContainer> | |||||
{brand} | |||||
</BrandContainer> | |||||
) | |||||
} | |||||
<CenterContainer> | |||||
{children} | |||||
</CenterContainer> | |||||
<ActionContainer> | |||||
{ | |||||
Boolean(menuLink as unknown) | |||||
&& ( | |||||
<MenuLinkContainer> | |||||
{menuLink} | |||||
</MenuLinkContainer> | |||||
) | |||||
} | |||||
{ | |||||
Boolean(userLink as unknown) | |||||
&& ( | |||||
<LinkContainer> | |||||
{userLink} | |||||
</LinkContainer> | |||||
) | |||||
} | |||||
</ActionContainer> | |||||
</ContainerComponent> | |||||
</Base> | |||||
) | |||||
} | |||||
export default TopBar |
@@ -0,0 +1,35 @@ | |||||
{ | |||||
// see https://www.typescriptlang.org/tsconfig to better understand tsconfigs | |||||
"include": ["src", "types"], | |||||
"compilerOptions": { | |||||
"module": "esnext", | |||||
"lib": ["dom", "esnext"], | |||||
"importHelpers": true, | |||||
// output .d.ts declaration files for consumers | |||||
"declaration": true, | |||||
// output .js.map sourcemap files for consumers | |||||
"sourceMap": true, | |||||
// match output dir to input dir. e.g. dist/index instead of dist/src/index | |||||
"rootDir": "./src", | |||||
// stricter type-checking for stronger correctness. Recommended by TS | |||||
"strict": true, | |||||
// linter checks for common issues | |||||
"noImplicitReturns": true, | |||||
"noFallthroughCasesInSwitch": true, | |||||
// noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative | |||||
"noUnusedLocals": true, | |||||
"noUnusedParameters": true, | |||||
// use Node's module resolution algorithm, instead of the legacy TS one | |||||
"moduleResolution": "node", | |||||
// transpile JSX to React.createElement | |||||
"jsx": "react", | |||||
// interop between ESM and CJS modules. Recommended by TS | |||||
"esModuleInterop": true, | |||||
// significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS | |||||
"skipLibCheck": true, | |||||
// error out if import and file system have a casing mismatch. Recommended by TS | |||||
"forceConsistentCasingInFileNames": true, | |||||
// `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` | |||||
"noEmit": true, | |||||
} | |||||
} |