@@ -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, | |||
} | |||
} |