1
1
mirror of https://github.com/c8r/x0.git synced 2024-08-17 01:10:32 +03:00

Add basic layout component

This commit is contained in:
Brent Jackson 2018-06-20 13:53:12 -04:00
parent a6e3e8f10e
commit 4556918cbd
6 changed files with 350 additions and 8 deletions

View File

@ -368,9 +368,13 @@ See the [example](https://github.com/c8r/x0/tree/master/examples/webpack-config)
**REMOVE BEFORE MERGING**
- [ ] pass RouterState props to view
- [ ] props.Component/children in custom apps
- [ ] changelog/docs
- [x] SidebarLayout
- [x] test getInitialProps
- [x] double check dynamic routing
- [x] pass RouterState props to view
- [x] props.children in custom app
- [x] require.context `_app`
- [x] peer deps
- [x] props.ignore
@ -383,7 +387,9 @@ See the [example](https://github.com/c8r/x0/tree/master/examples/webpack-config)
- [x] minimatch
- [x] move client modules to src
- [x] adjust resolve
- [x] props.Component in custom app?
- [ ] ~~Head component with react helmet~~ doesn't work
- [ ] ~~scope loader shim for jsx front matter~~
### Breaking

View File

@ -1,7 +1,7 @@
import React from 'react'
import * as scope from 'rebass'
import { Link } from 'react-router-dom'
import { ScopeProvider } from '../components'
import { ScopeProvider, SidebarLayout } from '../components'
import {
Flex,
Box,
@ -20,9 +20,20 @@ export default class App extends React.Component {
route,
children,
// alternative to props.children
Component,
render,
} = this.props
// built-in layout test
if (false) {
return (
<ScopeProvider scope={scope}>
<SidebarLayout {...this.props} />
</ScopeProvider>
)
}
return (
<ScopeProvider scope={scope}>
{false ? (

View File

@ -7,7 +7,7 @@
"x0": "cli.js"
},
"scripts": {
"start": "./cli.js docs -op 8888",
"start": "./cli.js docs -p 8888",
"build": "./cli.js build docs",
"test": "nyc ava -T 20s",
"cover": "nyc report --reporter=html --reporter=lcov"
@ -56,6 +56,7 @@
"mini-html-webpack-plugin": "^0.2.3",
"minimatch": "^3.0.4",
"pkg-conf": "^2.1.0",
"prop-types": "^15.6.2",
"react": "^16.4.1",
"react-dev-utils": "^5.0.1",
"react-dom": "^16.4.1",

319
src/SidebarLayout.js Normal file
View File

@ -0,0 +1,319 @@
import React from 'react'
import PropTypes from 'prop-types'
import {
Link as RouterLink,
NavLink as RouterNavLink
} from 'react-router-dom'
import styled from 'styled-components'
import {
Provider as RebassProvider,
Flex,
Box,
Fixed,
Container,
Text,
Close,
Toolbar,
Divider,
Heading,
NavLink,
BlockLink,
Button,
ButtonTransparent,
} from 'rebass'
import { borderColor, themeGet } from 'styled-system'
const breakpoint = `@media screen and (min-width: 48em)`
export const Root = styled(Flex)([], {
minHeight: '100vh'
})
export const Sidebar = styled('div')([], {
width: '256px',
height: '100vh',
flex: 'none',
overflowY: 'auto',
transition: 'transform .2s ease-out',
backgroundColor: '#fff',
borderRight: '1px solid',
position: 'fixed',
top: 0,
left: 0,
bottom: 0,
[breakpoint]: {
// transition: 'none'
}
}, props => ({
transform: props.open ? 'translateX(0)' : 'translateX(-100%)',
[breakpoint]: {
transform: 'none'
}
}), borderColor)
Sidebar.defaultProps = {
borderColor: 'gray'
}
export const Overlay = styled('div')([], {
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0,
})
export const MobileOnly = styled.div([], {
[breakpoint]: {
display: 'none'
},
})
export const MenuIcon = ({ size = 24, ...props }) =>
<svg
{...props}
viewBox='0 0 24 24'
width={size}
height={size}
fill='currentcolor'
>
<path d='M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z' />
</svg>
export const FocusButton = styled(Button)([], {
width: '2em',
height: '2em',
})
FocusButton.defaultProps = {
fontSize: 0,
px: '0.25em',
py: '0.25em',
m: 2,
color: 'black',
bg: 'gray'
}
export const Main = props =>
<Box
{...props}
is='main'
flex='1 1 auto'
w={1}
pl={[ null, null, 256 ]}
/>
export const MaxWidth = props =>
<Container
{...props}
maxWidth={768}
px={4}
pt={4}
pb={6}
/>
export const UL = styled('ul')([], {
listStyle: 'none',
margin: 0,
paddingLeft: 0,
paddingBottom: '48px',
})
export const LI = styled('li')([], {
})
const depthPad = ({ to = '' }) =>
(1 + to.split('/')
.filter(s => s.length)
.slice(1).length) * 16
const Link = styled(props => (
<NavLink
{...props}
is={RouterNavLink}
w={1}
pl={(depthPad(props) - 4) + 'px'}
/>
))([], props => ({
borderLeft: '4px solid',
borderColor: 'transparent',
'&.active, &:focus': {
color: themeGet('colors.blue', '#07c')(props),
outline: 'none',
},
'&:focus': {
borderColor: 'inherit',
}
}))
Link.defaultProps = {
to: ''
}
const unhyphenate = str => str.replace(/(\w)(-)(\w)/g, '$1 $3')
const upperFirst = str => str.charAt(0).toUpperCase() + str.slice(1)
const format = str => upperFirst(unhyphenate(str))
const NavBar = ({
title,
focus,
update,
}) =>
<Toolbar
color='inherit'
bg='transparent'>
<Heading
px={2}
fontSize={1}>
{title}
</Heading>
<Box mx='auto' />
{DEV && (
<FocusButton
title='focus'
onClick={e => update(toggle('focus'))}>
F
</FocusButton>
)}
</Toolbar>
export const Nav = ({
routes = [],
...props
}) =>
<React.Fragment>
<NavBar {...props} />
<Divider my={0} />
<UL>
{/* rename to route.type='section' */}
{routes.map(route => route.exact ? (
<LI key={route.section + '-exact'}>
<Link to={route.path} exact>
{format(route.name)} (index)
</Link>
</LI>
) : (
<LI key={route.key}>
{/^https?:\/\//.test(route.path) ? (
<NavLink pl={3} href={route.path}>
{route.name}
</NavLink>
) : (
<Link to={route.path} exact>
{format(route.name)}
</Link>
)}
</LI>
))}
</UL>
</React.Fragment>
export const Pagination = ({ previous, next }) =>
<Flex py={4} flexWrap='wrap'>
{previous && (
<BlockLink
py={2}
is={RouterLink}
to={previous.path}>
<Text mb={1}>Previous:</Text>
<Text
fontSize={3}
fontWeight='bold'>
{format(previous.name)}
</Text>
</BlockLink>
)}
<Box mx='auto' />
{next && (
<BlockLink
py={2}
is={RouterLink}
to={next.path}>
<Text mb={1}>Next:</Text>
<Text
fontSize={3}
fontWeight='bold'>
{format(next.name)}
</Text>
</BlockLink>
)}
</Flex>
// move to app
const toggle = key => state => ({ [key]: !state[key] })
const close = state => ({ menu: false })
export default class Layout extends React.Component {
static propTypes = {
// content: PropTypes.node.isRequired,
routes: PropTypes.array.isRequired
}
state = {
menu: false,
update: fn => this.setState(fn)
}
render () {
const {
routes = [],
children,
route,
location,
title = 'x0',
// theme,
// color,
} = this.props
const { menu, update } = this.state
const Wrapper = route && route.props && route.props.fullWidth
? React.Fragment
: MaxWidth
const index = routes.findIndex(r => r === route)
const pagination = {
previous: routes[index - 1],
next: routes[index + 1]
}
return (
<React.Fragment>
<MobileOnly>
<Toolbar px={0} color='inherit' bg='transparent'>
<ButtonTransparent
px={2}
borderRadius={0}
m={0}
mr='auto'
title='Toggle Menu'
onClick={e => update(toggle('menu'))}>
<MenuIcon />
</ButtonTransparent>
<Heading fontSize={1}>
{title}
</Heading>
<Box width={48} ml='auto' />
</Toolbar>
<Divider my={0} />
</MobileOnly>
<Root>
{menu && <Overlay onClick={e => update(close)} />}
<Sidebar
open={menu}
onClick={e => update(close)}>
<Nav
title={title}
routes={routes}
update={update}
/>
</Sidebar>
<Main tabIndex={menu ? -1 : undefined}>
<Wrapper>
{children}
<Pagination {...pagination} />
</Wrapper>
</Main>
</Root>
</React.Fragment>
)
}
}

View File

@ -40,9 +40,9 @@ const getComponents = req => req.keys()
const initialComponents = getComponents(req)
const DefaultApp = ({ render, routes }) => (
const DefaultApp = ({ children, routes }) => (
<Switch>
{render()}
{children}
<Route render={props => (
<FileList
{...props}
@ -98,7 +98,8 @@ export const getRoutes = async (components = initialComponents) => {
}
const RouterState = withRouter(({ render, ...props }) => {
const route = props.routes.find(r => r.path === props.location.pathname)
const { pathname } = props.location
const route = props.routes.find(r => r.path === pathname || r.href === pathname)
return render({ ...props, route })
})
@ -155,7 +156,8 @@ export default class Root extends React.Component {
{...router}
routes={routes}
render={render}
children={render()}
Component={render}
children={render(router)}
/>
)}
/>

View File

@ -6,3 +6,6 @@ export { default as FileList } from './FileList'
export { default as Catch } from './Catch'
export { default as ScrollTop } from './ScrollTop'
export { default as scope } from './scope'
// layouts
export { default as SidebarLayout } from './SidebarLayout'